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.
@@ -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
- PLACEHOLDER_REGEX = /\$?\{([^}]+)\}|\#\{([^}]+)\}/.freeze
9
- JS_FLOAT_PREFIX_RE = /^\s*([+-]?(?:(?:[0-9]+\.?[0-9]*)|(?:\.[0-9]+))(?:[eE][+-]?[0-9]+)?)/.freeze
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 eval_condition(condition, binding_ctx)
13
- return true if condition.nil? || condition.strip.empty?
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
- clean_cond = condition.strip
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.nil? || watch_expressions.empty?
24
-
25
- watch_expressions.each do |expr|
26
- clean_expr = expr.strip
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.validate!(clean_expr)
31
- results[expr] = binding_ctx.eval(clean_expr)
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[expr] = "Error: #{e.class.name}: #{e.message}"
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.nil? || template.empty?
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
- SafeASTValidator.validate!(expr)
46
- val = binding_ctx.eval(expr)
47
- val.is_a?(String) ? val : val.inspect
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 metric_expression.nil? || metric_expression.strip.empty?
136
+ def evaluate_metric(metric_expression, binding_ctx, safe: safe_evaluation_enabled?)
137
+ return nil if blank?(metric_expression)
56
138
 
57
- clean_expr = metric_expression.strip
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 correlation_expression.nil? || correlation_expression.strip.empty?
142
+ def evaluate_correlation(correlation_expression, binding_ctx, safe: safe_evaluation_enabled?)
143
+ return 'static-singleton' if blank?(correlation_expression)
65
144
 
66
- clean_expr = correlation_expression.strip
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
- return nil if value == true || value == false
74
- return nil if value.nil?
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
- if value.is_a?(Numeric)
77
- float_val = value.to_f
78
- return float_val if float_val.finite?
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
- return nil
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
- if value.is_a?(String)
84
- match = JS_FLOAT_PREFIX_RE.match(value)
85
- if match
86
- float_val = match[1].to_f
87
- return float_val if float_val.finite?
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 normalize_correlation_value(value)
95
- return 'static-singleton' if value.nil?
229
+ def step!(budget)
230
+ budget[0] -= 1
231
+ raise SecurityError, 'Expression step limit exceeded' if budget[0] < 0
232
+ end
96
233
 
97
- if value == true || value == false
98
- type_name = value ? 'true' : 'false'
99
- raise TypeError, "Correlation expression must evaluate to a string or number, got boolean (#{type_name})"
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
- if value.is_a?(String)
103
- if value.start_with?('Error: ')
104
- raise StandardError, value
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
- return value
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
- if value.is_a?(Integer)
111
- return value.to_s
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
- if value.is_a?(Float)
115
- return 'NaN' if value.nan?
116
- return 'Infinity' if value.infinite? && value.positive?
117
- return '-Infinity' if value.infinite? && value.negative?
118
- return value.to_i.to_s if (value % 1.0).zero?
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
- return value.to_s
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
- raise TypeError, "Correlation expression must evaluate to a string or number, got #{value.class.name}"
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