hyperprobe-agent 1.2.27.pre.1 → 1.2.27.pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +316 -87
- data/lib/hyperprobe/agent.rb +243 -144
- data/lib/hyperprobe/core/broker.rb +58 -88
- data/lib/hyperprobe/core/evaluator.rb +358 -68
- data/lib/hyperprobe/core/lexical_scope.rb +222 -0
- data/lib/hyperprobe/core/logger.rb +33 -12
- data/lib/hyperprobe/core/monitoring_engine.rb +294 -120
- data/lib/hyperprobe/core/safe_ast_validator.rb +140 -61
- data/lib/hyperprobe/core/safety.rb +92 -15
- data/lib/hyperprobe/core/serializer.rb +354 -342
- data/lib/hyperprobe/core/transports/java_grpc.rb +57 -0
- data/lib/hyperprobe/lambda.rb +44 -42
- data/lib/hyperprobe/protos/agent_descriptor.rb +7 -0
- data/lib/hyperprobe/protos/java_messages.rb +199 -0
- data/lib/hyperprobe/protos.rb +7 -7
- data/lib/hyperprobe/railtie.rb +6 -0
- data/lib/hyperprobe/version.rb +1 -1
- data/lib/hyperprobe.rb +149 -20
- metadata +25 -9
|
@@ -1,126 +1,416 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'safe_ast_validator'
|
|
4
|
+
require 'objspace' unless RUBY_PLATFORM.include?('java')
|
|
4
5
|
|
|
5
6
|
module HyperProbe
|
|
6
7
|
module Core
|
|
7
8
|
class Evaluator
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
MAX_STRING_BYTES = 4096
|
|
10
|
+
MAX_INTEGER_BITS = 1024
|
|
11
|
+
MAX_COLLECTION_ITEMS = 128
|
|
12
|
+
MAX_STEPS = 512
|
|
13
|
+
MAX_TEMPLATE_BYTES = 16_384
|
|
14
|
+
MAX_PLACEHOLDERS = 32
|
|
15
|
+
MEMORY_SIZE = ObjectSpace.method(:memsize_of) if ObjectSpace.respond_to?(:memsize_of)
|
|
16
|
+
JS_FLOAT_PREFIX_RE = /\A\s*([+-]?(?:(?:[0-9]+\.?[0-9]*)|(?:\.[0-9]+))(?:[eE][+-]?[0-9]+)?)/.freeze
|
|
17
|
+
CLASS = Object.instance_method(:class)
|
|
18
|
+
IDENTITY = BasicObject.instance_method(:equal?)
|
|
19
|
+
LOCAL_GET = Binding.instance_method(:local_variable_get)
|
|
20
|
+
LOCAL_DEFINED = Binding.instance_method(:local_variable_defined?)
|
|
21
|
+
BINDING_EVAL = Binding.instance_method(:eval)
|
|
22
|
+
TYPES = { string: String, integer: Integer, float: Float, symbol: Symbol,
|
|
23
|
+
array: Array, hash: Hash, binding: Binding, nil: NilClass,
|
|
24
|
+
true: TrueClass, false: FalseClass }.freeze
|
|
25
|
+
SYMBOL_NAME = if Symbol.method_defined?(:name)
|
|
26
|
+
Symbol.instance_method(:name)
|
|
27
|
+
elsif RUBY_PLATFORM.include?('java')
|
|
28
|
+
# JRuby 9.3 returns a constant-sized copy-on-write wrapper.
|
|
29
|
+
Symbol.instance_method(:to_s)
|
|
30
|
+
end
|
|
31
|
+
# Bind captured builtin implementations, bypassing singleton overrides.
|
|
32
|
+
# Only allowlisted interpreter operations reach this table.
|
|
33
|
+
BUILTINS = {
|
|
34
|
+
string: %i[bytesize byteslice [] length + == < <= > >= inspect to_f],
|
|
35
|
+
integer: %i[bit_length + - * / % == < <= > >= +@ -@ to_f to_s],
|
|
36
|
+
float: %i[+ - * / % == < <= > >= +@ -@ to_f to_s finite? nan? infinite? to_i],
|
|
37
|
+
symbol: %i[== name inspect],
|
|
38
|
+
array: %i[length []],
|
|
39
|
+
hash: %i[length each_pair compare_by_identity?]
|
|
40
|
+
}.each_with_object({}) do |(kind, methods), result|
|
|
41
|
+
result[kind] = methods.to_h do |method|
|
|
42
|
+
implementation = kind == :symbol && method == :name ? SYMBOL_NAME : TYPES.fetch(kind).instance_method(method)
|
|
43
|
+
[method, implementation]
|
|
44
|
+
end.freeze
|
|
45
|
+
end.freeze
|
|
10
46
|
|
|
11
47
|
class << self
|
|
12
|
-
def
|
|
13
|
-
|
|
48
|
+
def safe_evaluation_enabled?(disable = nil)
|
|
49
|
+
disable = ENV['HYPERPROBE_DISABLE_SAFE_EVALUATION'] if disable.nil?
|
|
50
|
+
!(disable == true || (disable.is_a?(String) && disable.strip.downcase == 'true'))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def eval_condition(condition, binding_ctx, safe: safe_evaluation_enabled?)
|
|
54
|
+
return true if blank?(condition)
|
|
14
55
|
|
|
15
|
-
|
|
16
|
-
SafeASTValidator.validate!(clean_cond)
|
|
17
|
-
result = binding_ctx.eval(clean_cond)
|
|
18
|
-
!!result
|
|
56
|
+
truthy?(evaluate(condition, binding_ctx, safe: safe))
|
|
19
57
|
end
|
|
20
58
|
|
|
21
|
-
def evaluate_watches(watch_expressions, binding_ctx)
|
|
59
|
+
def evaluate_watches(watch_expressions, binding_ctx, safe: safe_evaluation_enabled?)
|
|
22
60
|
results = {}
|
|
23
|
-
return results if watch_expressions
|
|
24
|
-
|
|
25
|
-
watch_expressions
|
|
26
|
-
|
|
27
|
-
next if clean_expr.empty?
|
|
61
|
+
return results if kind(watch_expressions) == :nil
|
|
62
|
+
raise SecurityError, 'Watches must be an exact builtin Array' unless kind(watch_expressions) == :array
|
|
63
|
+
size = builtin(watch_expressions, :length)
|
|
64
|
+
raise SecurityError, 'Too many watch expressions' if size > MAX_COLLECTION_ITEMS
|
|
28
65
|
|
|
66
|
+
size.times do |index|
|
|
67
|
+
expression = nil
|
|
29
68
|
begin
|
|
30
|
-
SafeASTValidator.
|
|
31
|
-
|
|
69
|
+
expression = SafeASTValidator.source(builtin(watch_expressions, :[], index))
|
|
70
|
+
next if expression.strip.empty?
|
|
71
|
+
results[expression] = evaluate(expression, binding_ctx, safe: safe)
|
|
32
72
|
rescue StandardError, ScriptError, SecurityError => e
|
|
33
|
-
results[
|
|
73
|
+
results[expression || "<invalid expression #{index}>"] = "Error: #{e.class.name}: #{e.message}"
|
|
34
74
|
end
|
|
35
75
|
end
|
|
36
76
|
results
|
|
37
77
|
end
|
|
38
78
|
|
|
39
|
-
def evaluate_log_template(template, binding_ctx)
|
|
40
|
-
return '' if template
|
|
79
|
+
def evaluate_log_template(template, binding_ctx, safe: safe_evaluation_enabled?)
|
|
80
|
+
return '' if kind(template) == :nil
|
|
81
|
+
|
|
82
|
+
text = SafeASTValidator.source(template, MAX_TEMPLATE_BYTES)
|
|
83
|
+
characters = text.chars
|
|
84
|
+
output = String.new
|
|
85
|
+
cursor = 0
|
|
86
|
+
placeholders = 0
|
|
87
|
+
while cursor < characters.length
|
|
88
|
+
unless ['$', '#'].include?(characters[cursor]) && characters[cursor + 1] == '{'
|
|
89
|
+
output << characters[cursor]
|
|
90
|
+
cursor += 1
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
placeholders += 1
|
|
95
|
+
raise SecurityError, 'Too many log placeholders' if placeholders > MAX_PLACEHOLDERS
|
|
96
|
+
start = cursor + 2
|
|
97
|
+
cursor = start
|
|
98
|
+
depth = 1
|
|
99
|
+
quote = nil
|
|
100
|
+
escaped = false
|
|
101
|
+
while cursor < characters.length && depth > 0
|
|
102
|
+
char = characters[cursor]
|
|
103
|
+
if quote
|
|
104
|
+
if escaped
|
|
105
|
+
escaped = false
|
|
106
|
+
elsif char == '\\'
|
|
107
|
+
escaped = true
|
|
108
|
+
elsif char == quote
|
|
109
|
+
quote = nil
|
|
110
|
+
end
|
|
111
|
+
elsif ["'", '"'].include?(char)
|
|
112
|
+
quote = char
|
|
113
|
+
elsif char == '{'
|
|
114
|
+
depth += 1
|
|
115
|
+
elsif char == '}'
|
|
116
|
+
depth -= 1
|
|
117
|
+
end
|
|
118
|
+
cursor += 1
|
|
119
|
+
end
|
|
41
120
|
|
|
42
|
-
template.gsub(PLACEHOLDER_REGEX) do |_match|
|
|
43
|
-
expr = (Regexp.last_match(1) || Regexp.last_match(2)).strip
|
|
44
121
|
begin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
122
|
+
raise ArgumentError, 'Unterminated log placeholder' unless depth.zero?
|
|
123
|
+
value = evaluate(characters[start...(cursor - 1)].join, binding_ctx, safe: safe)
|
|
124
|
+
rendered = display(value, [MAX_STEPS], 0)
|
|
125
|
+
raise SecurityError, 'Log output is too large' if output.bytesize + rendered.bytesize > MAX_TEMPLATE_BYTES
|
|
126
|
+
output << rendered
|
|
48
127
|
rescue StandardError, ScriptError, SecurityError => e
|
|
49
|
-
"<Error: #{e.class.name}: #{e.message}>"
|
|
128
|
+
output << "<Error: #{e.class.name}: #{e.message}>"
|
|
50
129
|
end
|
|
130
|
+
raise SecurityError, 'Log output is too large' if output.bytesize > MAX_TEMPLATE_BYTES
|
|
51
131
|
end
|
|
132
|
+
raise SecurityError, 'Log output is too large' if output.bytesize > MAX_TEMPLATE_BYTES
|
|
133
|
+
output
|
|
52
134
|
end
|
|
53
135
|
|
|
54
|
-
def evaluate_metric(metric_expression, binding_ctx)
|
|
55
|
-
return nil if
|
|
136
|
+
def evaluate_metric(metric_expression, binding_ctx, safe: safe_evaluation_enabled?)
|
|
137
|
+
return nil if blank?(metric_expression)
|
|
56
138
|
|
|
57
|
-
|
|
58
|
-
SafeASTValidator.validate!(clean_expr)
|
|
59
|
-
raw_val = binding_ctx.eval(clean_expr)
|
|
60
|
-
coerce_metric_value(raw_val)
|
|
139
|
+
coerce_metric_value(evaluate(metric_expression, binding_ctx, safe: safe))
|
|
61
140
|
end
|
|
62
141
|
|
|
63
|
-
def evaluate_correlation(correlation_expression, binding_ctx)
|
|
64
|
-
return 'static-singleton' if
|
|
142
|
+
def evaluate_correlation(correlation_expression, binding_ctx, safe: safe_evaluation_enabled?)
|
|
143
|
+
return 'static-singleton' if blank?(correlation_expression)
|
|
65
144
|
|
|
66
|
-
|
|
67
|
-
SafeASTValidator.validate!(clean_expr)
|
|
68
|
-
raw_val = binding_ctx.eval(clean_expr)
|
|
69
|
-
normalize_correlation_value(raw_val)
|
|
145
|
+
normalize_correlation_value(evaluate(correlation_expression, binding_ctx, safe: safe))
|
|
70
146
|
end
|
|
71
147
|
|
|
72
148
|
def coerce_metric_value(value)
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
case kind(value)
|
|
150
|
+
when :integer, :float
|
|
151
|
+
checked(value)
|
|
152
|
+
number = builtin(value, :to_f)
|
|
153
|
+
when :string
|
|
154
|
+
match = JS_FLOAT_PREFIX_RE.match(copy_string(value))
|
|
155
|
+
number = match && match[1].to_f
|
|
156
|
+
else
|
|
157
|
+
return nil
|
|
158
|
+
end
|
|
159
|
+
number && builtin(number, :finite?) ? number : nil
|
|
160
|
+
end
|
|
75
161
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
def normalize_correlation_value(value)
|
|
163
|
+
case kind(value)
|
|
164
|
+
when :nil
|
|
165
|
+
'static-singleton'
|
|
166
|
+
when :true, :false
|
|
167
|
+
raise TypeError, 'Correlation expression must evaluate to a string or number, got boolean'
|
|
168
|
+
when :string
|
|
169
|
+
copy = copy_string(value)
|
|
170
|
+
raise StandardError, copy if copy.start_with?('Error: ')
|
|
171
|
+
copy.freeze
|
|
172
|
+
when :integer
|
|
173
|
+
builtin(checked(value), :to_s).freeze
|
|
174
|
+
when :float
|
|
175
|
+
return 'NaN' if builtin(value, :nan?)
|
|
176
|
+
infinite = builtin(value, :infinite?)
|
|
177
|
+
return infinite == 1 ? 'Infinity' : '-Infinity' if infinite
|
|
178
|
+
return builtin(checked(builtin(value, :to_i)), :to_s).freeze if builtin(value, :%, 1.0).zero?
|
|
79
179
|
|
|
80
|
-
|
|
180
|
+
builtin(value, :to_s).freeze
|
|
181
|
+
else
|
|
182
|
+
type = { array: 'Array', hash: 'Hash', symbol: 'Symbol' }[kind(value)] || 'unsupported object'
|
|
183
|
+
raise TypeError, "Correlation expression must evaluate to a string or number, got #{type}"
|
|
81
184
|
end
|
|
185
|
+
end
|
|
82
186
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def kind(value)
|
|
190
|
+
type = CLASS.bind(value).call
|
|
191
|
+
TYPES.each { |name, trusted| return name if IDENTITY.bind(trusted).call(type) }
|
|
192
|
+
:unsupported
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def builtin(value, method, *args, &block)
|
|
196
|
+
implementation = BUILTINS.fetch(kind(value)).fetch(method)
|
|
197
|
+
implementation.bind(value).call(*args, &block)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def truthy?(value)
|
|
201
|
+
!IDENTITY.bind(value).call(nil) && !IDENTITY.bind(value).call(false)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def blank?(expression)
|
|
205
|
+
kind(expression) == :nil || SafeASTValidator.source(expression).strip.empty?
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def copy_string(value)
|
|
209
|
+
raise SecurityError, 'String exceeds expression limit' if builtin(value, :bytesize) > MAX_STRING_BYTES
|
|
210
|
+
copy = builtin(value, :byteslice, 0, MAX_STRING_BYTES + 1)
|
|
211
|
+
raise SecurityError, 'String exceeds expression limit' if copy.bytesize > MAX_STRING_BYTES
|
|
212
|
+
copy
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def checked(value)
|
|
216
|
+
case kind(value)
|
|
217
|
+
when :integer
|
|
218
|
+
raise SecurityError, 'Number exceeds expression limit' if builtin(value, :bit_length) > MAX_INTEGER_BITS
|
|
219
|
+
when :string
|
|
220
|
+
return copy_string(value)
|
|
221
|
+
when :symbol
|
|
222
|
+
if builtin(builtin(value, :name), :bytesize) > MAX_STRING_BYTES
|
|
223
|
+
raise SecurityError, 'Symbol exceeds expression limit'
|
|
88
224
|
end
|
|
89
225
|
end
|
|
90
|
-
|
|
91
|
-
nil
|
|
226
|
+
value
|
|
92
227
|
end
|
|
93
228
|
|
|
94
|
-
def
|
|
95
|
-
|
|
229
|
+
def step!(budget)
|
|
230
|
+
budget[0] -= 1
|
|
231
|
+
raise SecurityError, 'Expression step limit exceeded' if budget[0] < 0
|
|
232
|
+
end
|
|
96
233
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
234
|
+
def evaluate(expression, binding_ctx, safe:)
|
|
235
|
+
raise SecurityError, 'Expected an exact builtin Binding' unless kind(binding_ctx) == :binding
|
|
236
|
+
if safe == false
|
|
237
|
+
# Explicit opt-in executes Ruby in the hit binding, with application
|
|
238
|
+
# permissions and side effects. Source and output size limits remain.
|
|
239
|
+
return BINDING_EVAL.bind(binding_ctx).call(SafeASTValidator.source(expression), '(hyperprobe)', 1)
|
|
100
240
|
end
|
|
101
241
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
242
|
+
tree = SafeASTValidator.parse!(expression)
|
|
243
|
+
execute(tree, binding_ctx, [MAX_STEPS])
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def execute(node, binding_ctx, budget)
|
|
247
|
+
step!(budget)
|
|
248
|
+
case node[0]
|
|
249
|
+
when :literal
|
|
250
|
+
checked(node[1])
|
|
251
|
+
when :local
|
|
252
|
+
unless LOCAL_DEFINED.bind(binding_ctx).call(node[1])
|
|
253
|
+
raise NameError, "Unknown local variable '#{node[1]}'"
|
|
254
|
+
end
|
|
255
|
+
checked(LOCAL_GET.bind(binding_ctx).call(node[1]))
|
|
256
|
+
when :array
|
|
257
|
+
node[1].map { |item| execute(item, binding_ctx, budget) }
|
|
258
|
+
when :hash
|
|
259
|
+
node[1].each_with_object({}) do |(key_node, value_node), hash|
|
|
260
|
+
key = primitive_key(execute(key_node, binding_ctx, budget))
|
|
261
|
+
hash[key] = execute(value_node, binding_ctx, budget)
|
|
262
|
+
end
|
|
263
|
+
when :lookup
|
|
264
|
+
receiver = execute(node[1], binding_ctx, budget)
|
|
265
|
+
index = execute(node[2], binding_ctx, budget)
|
|
266
|
+
lookup(receiver, index, budget)
|
|
267
|
+
when :length
|
|
268
|
+
receiver = execute(node[1], binding_ctx, budget)
|
|
269
|
+
unless %i[string array hash].include?(kind(receiver))
|
|
270
|
+
raise SecurityError, 'length/size requires an exact builtin String, Array or Hash'
|
|
105
271
|
end
|
|
272
|
+
builtin(receiver, :length)
|
|
273
|
+
when :conditional
|
|
274
|
+
branch = truthy?(execute(node[1], binding_ctx, budget)) ? node[2] : node[3]
|
|
275
|
+
execute(branch, binding_ctx, budget)
|
|
276
|
+
when :unary
|
|
277
|
+
value = execute(node[2], binding_ctx, budget)
|
|
278
|
+
return !truthy?(value) if %i[! not].include?(node[1])
|
|
279
|
+
raise SecurityError, 'Unary arithmetic requires builtin numbers' unless numeric?(value)
|
|
280
|
+
checked(builtin(value, node[1]))
|
|
281
|
+
when :binary
|
|
282
|
+
left = execute(node[2], binding_ctx, budget)
|
|
283
|
+
operator = node[1]
|
|
284
|
+
if %i[&& and].include?(operator)
|
|
285
|
+
return truthy?(left) ? execute(node[3], binding_ctx, budget) : left
|
|
286
|
+
elsif %i[|| or].include?(operator)
|
|
287
|
+
return truthy?(left) ? left : execute(node[3], binding_ctx, budget)
|
|
288
|
+
end
|
|
289
|
+
right = execute(node[3], binding_ctx, budget)
|
|
290
|
+
binary(left, operator, right)
|
|
291
|
+
else
|
|
292
|
+
raise SecurityError, 'Unsupported expression instruction'
|
|
293
|
+
end
|
|
294
|
+
end
|
|
106
295
|
|
|
107
|
-
|
|
296
|
+
def numeric?(value)
|
|
297
|
+
%i[integer float].include?(kind(value))
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def primitive_key(value)
|
|
301
|
+
unless %i[string symbol integer float nil true false].include?(kind(value))
|
|
302
|
+
raise SecurityError, 'Hash keys must be builtin primitives'
|
|
303
|
+
end
|
|
304
|
+
if kind(value) == :float && !builtin(value, :finite?)
|
|
305
|
+
raise SecurityError, 'Hash keys must be finite'
|
|
108
306
|
end
|
|
307
|
+
# Numeric and symbol objects cannot carry singleton overrides. Strings
|
|
308
|
+
# must be copied before insertion, which invokes hash/eql? internally.
|
|
309
|
+
checked(value)
|
|
310
|
+
end
|
|
109
311
|
|
|
110
|
-
|
|
111
|
-
|
|
312
|
+
def lookup(receiver, index, budget)
|
|
313
|
+
case kind(receiver)
|
|
314
|
+
when :array, :string
|
|
315
|
+
raise SecurityError, 'Subscript must be a builtin Integer' unless kind(index) == :integer
|
|
316
|
+
# Oversized indices must not reach native long conversion.
|
|
317
|
+
return nil if builtin(index, :bit_length) > 62
|
|
318
|
+
checked(builtin(receiver, :[], index))
|
|
319
|
+
when :hash
|
|
320
|
+
key = primitive_key(index)
|
|
321
|
+
raise SecurityError, 'Identity hash lookup is unsupported' if builtin(receiver, :compare_by_identity?)
|
|
322
|
+
raise SecurityError, 'Hash exceeds lookup limit' if builtin(receiver, :length) > MAX_COLLECTION_ITEMS
|
|
323
|
+
if defined?(MEMORY_SIZE) && MEMORY_SIZE.call(receiver) > 131_072
|
|
324
|
+
raise SecurityError, 'Hash storage exceeds lookup limit'
|
|
325
|
+
end
|
|
326
|
+
# Hash#[]/fetch can execute default procs or custom hash/eql? keys.
|
|
327
|
+
# A bounded scan avoids all of those hooks, even on missing keys.
|
|
328
|
+
builtin(receiver, :each_pair) do |stored, value|
|
|
329
|
+
step!(budget)
|
|
330
|
+
stored = primitive_key(stored)
|
|
331
|
+
next unless kind(stored) == kind(key)
|
|
332
|
+
return checked(value) if primitive_equal?(stored, key)
|
|
333
|
+
end
|
|
334
|
+
nil
|
|
335
|
+
else
|
|
336
|
+
raise SecurityError, 'Subscript requires an exact builtin Array, Hash or String'
|
|
112
337
|
end
|
|
338
|
+
end
|
|
113
339
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
340
|
+
def primitive_equal?(left, right)
|
|
341
|
+
left_kind = kind(left)
|
|
342
|
+
right_kind = kind(right)
|
|
343
|
+
allowed = %i[string symbol integer float nil true false]
|
|
344
|
+
unless allowed.include?(left_kind) && allowed.include?(right_kind)
|
|
345
|
+
raise SecurityError, 'Comparison requires builtin primitives'
|
|
346
|
+
end
|
|
347
|
+
if numeric?(left) && numeric?(right)
|
|
348
|
+
builtin(left, :==, right)
|
|
349
|
+
elsif left_kind != right_kind
|
|
350
|
+
false
|
|
351
|
+
elsif %i[nil true false].include?(left_kind)
|
|
352
|
+
true
|
|
353
|
+
else
|
|
354
|
+
builtin(left, :==, right)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
119
357
|
|
|
120
|
-
|
|
358
|
+
def binary(left, operator, right)
|
|
359
|
+
if %i[== !=].include?(operator)
|
|
360
|
+
equal = primitive_equal?(left, right)
|
|
361
|
+
return operator == :== ? equal : !equal
|
|
362
|
+
end
|
|
363
|
+
if numeric?(left) && numeric?(right)
|
|
364
|
+
result = builtin(left, operator, right)
|
|
365
|
+
if kind(result) == :float && !builtin(result, :finite?)
|
|
366
|
+
raise SecurityError, 'Non-finite arithmetic result'
|
|
367
|
+
end
|
|
368
|
+
return checked(result)
|
|
121
369
|
end
|
|
370
|
+
if kind(left) == :string && kind(right) == :string && %i[+ < <= > >=].include?(operator)
|
|
371
|
+
if operator == :+ && builtin(left, :bytesize) + builtin(right, :bytesize) > MAX_STRING_BYTES
|
|
372
|
+
raise SecurityError, 'String computation exceeds expression limit'
|
|
373
|
+
end
|
|
374
|
+
return builtin(left, operator, right)
|
|
375
|
+
end
|
|
376
|
+
raise SecurityError, 'Operator requires builtin numbers or supported string operands'
|
|
377
|
+
end
|
|
122
378
|
|
|
123
|
-
|
|
379
|
+
def display(value, budget, depth, nested = false)
|
|
380
|
+
step!(budget)
|
|
381
|
+
raise SecurityError, 'Log value nesting limit exceeded' if depth > SafeASTValidator::MAX_DEPTH
|
|
382
|
+
value = checked(value)
|
|
383
|
+
result = case kind(value)
|
|
384
|
+
when :string
|
|
385
|
+
nested ? builtin(value, :inspect) : value
|
|
386
|
+
when :integer, :float
|
|
387
|
+
builtin(value, :to_s)
|
|
388
|
+
when :symbol
|
|
389
|
+
builtin(value, :inspect)
|
|
390
|
+
when :nil, :true, :false
|
|
391
|
+
{ nil: 'nil', true: 'true', false: 'false' }.fetch(kind(value))
|
|
392
|
+
when :array
|
|
393
|
+
size = builtin(value, :length)
|
|
394
|
+
raise SecurityError, 'Log collection exceeds limit' if size > MAX_COLLECTION_ITEMS
|
|
395
|
+
parts = []
|
|
396
|
+
size.times do |index|
|
|
397
|
+
parts << display(builtin(value, :[], index), budget, depth + 1, true)
|
|
398
|
+
raise SecurityError, 'Log value exceeds limit' if parts.sum(&:bytesize) > MAX_STRING_BYTES
|
|
399
|
+
end
|
|
400
|
+
"[#{parts.join(', ')}]"
|
|
401
|
+
when :hash
|
|
402
|
+
raise SecurityError, 'Log collection exceeds limit' if builtin(value, :length) > MAX_COLLECTION_ITEMS
|
|
403
|
+
parts = []
|
|
404
|
+
builtin(value, :each_pair) do |key, item|
|
|
405
|
+
parts << "#{display(key, budget, depth + 1, true)}=>#{display(item, budget, depth + 1, true)}"
|
|
406
|
+
raise SecurityError, 'Log value exceeds limit' if parts.sum(&:bytesize) > MAX_STRING_BYTES
|
|
407
|
+
end
|
|
408
|
+
"{#{parts.join(', ')}}"
|
|
409
|
+
else
|
|
410
|
+
raise SecurityError, 'Log rendering requires builtin primitives or containers'
|
|
411
|
+
end
|
|
412
|
+
raise SecurityError, 'Log value exceeds limit' if result.bytesize > MAX_STRING_BYTES
|
|
413
|
+
result
|
|
124
414
|
end
|
|
125
415
|
end
|
|
126
416
|
end
|