scnr-introspector 0.1 → 0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5682baf4ae51753f560e40d01c437b110fb798e9dda4e780799832e397177597
4
- data.tar.gz: 6ccac3fd0802d210ae62105c97a27b407337df4a5460e556836201744f6f54c4
3
+ metadata.gz: '06581df63125568c3bcd2c6f996bc81e20426d750397806e47ccf7d31a6a6ff2'
4
+ data.tar.gz: 814cd55b04084b83615ffcd3433b336ee5ffd3e5b65539b0bcdd7d75d9e16c8b
5
5
  SHA512:
6
- metadata.gz: b3256314a27a42c47c2e37b770d953827d487db5cb91f5fb01895a865bebfe7eff0078b435143575d56dbcc11d43accefcabc57ff35891d38d0631c96fc5f86b
7
- data.tar.gz: cb0d31f2ad427fc23f04b13092dabf0d801badee3f4cedd41f06a1376535ad254055fca29f6c81ac2cf65ad1b3a696838497777108404c3fdd0d31154c1f2b3d
6
+ metadata.gz: bc324b142de05b4152a54d1e501e018fbc84ec3264edadc4b4640ad02253667755b28f2ed2c68929c11f305ba57c5584f9efda5784c74859d8154ca700ad0ef2
7
+ data.tar.gz: 8c6d81443c7b86f417a86fb671719863fc2c9e463644b5b9811d902e26e7e680d72bef7cfe9793d89814d5cae8700b6cca6edd1a44ed88d49815c0a9e01a6490
@@ -1,3 +1,5 @@
1
+ require_relative '../utilities/my_method_source/code_helpers'
2
+
1
3
  module SCNR
2
4
  class Introspector
3
5
  class DataFlow
@@ -7,6 +9,10 @@ class Sink
7
9
  attr_accessor :object
8
10
 
9
11
  attr_accessor :method_name
12
+ attr_accessor :method_source
13
+ attr_accessor :method_source_location
14
+
15
+ attr_accessor :source
10
16
 
11
17
  attr_accessor :arguments
12
18
 
@@ -23,6 +29,27 @@ class Sink
23
29
 
24
30
  send( "#{k}=", v )
25
31
  end
32
+
33
+ if !@method_source && @method_source_location
34
+ filepath = @method_source_location.first
35
+ lineno = @method_source_location.last
36
+
37
+ if File.exists? filepath
38
+ File.open filepath do |f|
39
+ begin
40
+ @method_source = MyMethodSource::CodeHelpers.expression_at( File.open( f ), lineno )
41
+ rescue SyntaxError
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ if !@source && @backtrace
48
+ source_location = @backtrace.first.split( ':' ).first
49
+ if File.exists? source_location
50
+ @source = IO.read( source_location )
51
+ end
52
+ end
26
53
  end
27
54
 
28
55
  def marshal_dump
@@ -1,3 +1,5 @@
1
+ require 'thread'
2
+
1
3
  module SCNR
2
4
  class Introspector
3
5
  class ExecutionFlow
@@ -26,6 +28,8 @@ class Point
26
28
  # Event name.
27
29
  attr_accessor :event
28
30
 
31
+ attr_accessor :source
32
+
29
33
  # @param [Hash] options
30
34
  def initialize( options = {} )
31
35
  options.each do |k, v|
@@ -76,10 +80,26 @@ class Point
76
80
  line_number: tp.lineno,
77
81
  class_name: defined_class,
78
82
  method_name: tp.method_id,
79
- event: tp.event
83
+ event: tp.event,
84
+ source: source_line( tp.path, tp.lineno )
80
85
  })
81
86
  end
87
+
88
+ def source_line_mutex( &block )
89
+ (@mutex ||= Mutex.new).synchronize( &block )
90
+ end
91
+
92
+ def source_line( path, line )
93
+ return if !path || !line
94
+
95
+ source_line_mutex do
96
+ @@lines ||= {}
97
+ @@lines[path] ||= IO.readlines( path )
98
+ @@lines[path][line-1]
99
+ end
100
+ end
82
101
  end
102
+ source_line_mutex {}
83
103
 
84
104
  end
85
105
 
@@ -6,6 +6,8 @@ class Introspector
6
6
 
7
7
  class ExecutionFlow
8
8
 
9
+ ALLOWED_EVENTS = Set.new([:line, :call, :c_call])
10
+
9
11
  # @return [Scope]
10
12
  attr_accessor :scope
11
13
 
@@ -49,6 +51,7 @@ class ExecutionFlow
49
51
  # `self`
50
52
  def trace( &block )
51
53
  TracePoint.new do |tp|
54
+ next if !ALLOWED_EVENTS.include? tp.event
52
55
  next if @scope.out?( tp.path )
53
56
 
54
57
  @points << create_point_from_trace_point( tp )
@@ -0,0 +1,156 @@
1
+ module MyMethodSource
2
+
3
+ module CodeHelpers
4
+ # Retrieve the first expression starting on the given line of the given file.
5
+ #
6
+ # This is useful to get module or method source code.
7
+ #
8
+ # @param [Array<String>, File, String] file The file to parse, either as a File or as
9
+ # @param [Integer] line_number The line number at which to look.
10
+ # NOTE: The first line in a file is
11
+ # line 1!
12
+ # @param [Hash] options The optional configuration parameters.
13
+ # @option options [Boolean] :strict If set to true, then only completely
14
+ # valid expressions are returned. Otherwise heuristics are used to extract
15
+ # expressions that may have been valid inside an eval.
16
+ # @option options [Integer] :consume A number of lines to automatically
17
+ # consume (add to the expression buffer) without checking for validity.
18
+ # @return [String] The first complete expression
19
+ # @raise [SyntaxError] If the first complete expression can't be identified
20
+ def expression_at(file, line_number, options={})
21
+ options = {
22
+ :strict => false,
23
+ :consume => 0
24
+ }.merge!(options)
25
+
26
+ lines = file.is_a?(Array) ? file : file.each_line.to_a
27
+
28
+ relevant_lines = lines[(line_number - 1)..-1] || []
29
+
30
+ extract_first_expression(relevant_lines, options[:consume])
31
+ rescue SyntaxError => e
32
+ raise if options[:strict]
33
+
34
+ begin
35
+ extract_first_expression(relevant_lines) do |code|
36
+ code.gsub(/\#\{.*?\}/, "temp")
37
+ end
38
+ rescue SyntaxError
39
+ raise e
40
+ end
41
+ end
42
+
43
+ # Retrieve the comment describing the expression on the given line of the given file.
44
+ #
45
+ # This is useful to get module or method documentation.
46
+ #
47
+ # @param [Array<String>, File, String] file The file to parse, either as a File or as
48
+ # a String or an Array of lines.
49
+ # @param [Integer] line_number The line number at which to look.
50
+ # NOTE: The first line in a file is line 1!
51
+ # @return [String] The comment
52
+ def comment_describing(file, line_number)
53
+ lines = file.is_a?(Array) ? file : file.each_line.to_a
54
+
55
+ extract_last_comment(lines[0..(line_number - 2)])
56
+ end
57
+
58
+ # Determine if a string of code is a complete Ruby expression.
59
+ # @param [String] code The code to validate.
60
+ # @return [Boolean] Whether or not the code is a complete Ruby expression.
61
+ # @raise [SyntaxError] Any SyntaxError that does not represent incompleteness.
62
+ # @example
63
+ # complete_expression?("class Hello") #=> false
64
+ # complete_expression?("class Hello; end") #=> true
65
+ # complete_expression?("class 123") #=> SyntaxError: unexpected tINTEGER
66
+ def complete_expression?(str)
67
+ old_verbose = $VERBOSE
68
+ $VERBOSE = nil
69
+
70
+ catch(:valid) do
71
+ eval("BEGIN{throw :valid}\n#{str}")
72
+ end
73
+
74
+ # Assert that a line which ends with a , or \ is incomplete.
75
+ str !~ /[,\\]\s*\z/
76
+ rescue IncompleteExpression
77
+ false
78
+ ensure
79
+ $VERBOSE = old_verbose
80
+ end
81
+
82
+ private
83
+
84
+ # Get the first expression from the input.
85
+ #
86
+ # @param [Array<String>] lines
87
+ # @param [Integer] consume A number of lines to automatically
88
+ # consume (add to the expression buffer) without checking for validity.
89
+ # @yield a clean-up function to run before checking for complete_expression
90
+ # @return [String] a valid ruby expression
91
+ # @raise [SyntaxError]
92
+ def extract_first_expression(lines, consume=0, &block)
93
+ code = consume.zero? ? +"" : lines.slice!(0..(consume - 1)).join
94
+
95
+ lines.each do |v|
96
+ code << v
97
+ return code if complete_expression?(block ? block.call(code) : code)
98
+ end
99
+ raise SyntaxError, "unexpected $end"
100
+ end
101
+
102
+ # Get the last comment from the input.
103
+ #
104
+ # @param [Array<String>] lines
105
+ # @return [String]
106
+ def extract_last_comment(lines)
107
+ buffer = +""
108
+
109
+ lines.each do |line|
110
+ # Add any line that is a valid ruby comment,
111
+ # but clear as soon as we hit a non comment line.
112
+ if (line =~ /^\s*#/) || (line =~ /^\s*$/)
113
+ buffer << line.lstrip
114
+ else
115
+ buffer.clear
116
+ end
117
+ end
118
+
119
+ buffer
120
+ end
121
+
122
+ # An exception matcher that matches only subsets of SyntaxErrors that can be
123
+ # fixed by adding more input to the buffer.
124
+ module IncompleteExpression
125
+ GENERIC_REGEXPS = [
126
+ /unexpected (\$end|end-of-file|end-of-input|END_OF_FILE)/, # mri, jruby, ruby-2.0, ironruby
127
+ /embedded document meets end of file/, # =begin
128
+ /unterminated (quoted string|string|regexp|list) meets end of file/, # "quoted string" is ironruby
129
+ /can't find string ".*" anywhere before EOF/, # rbx and jruby
130
+ /missing 'end' for/, /expecting kWHEN/ # rbx
131
+ ]
132
+
133
+ RBX_ONLY_REGEXPS = [
134
+ /expecting '[})\]]'(?:$|:)/, /expecting keyword_end/
135
+ ]
136
+
137
+ def self.===(ex)
138
+ return false unless SyntaxError === ex
139
+ case ex.message
140
+ when *GENERIC_REGEXPS
141
+ true
142
+ when *RBX_ONLY_REGEXPS
143
+ rbx?
144
+ else
145
+ false
146
+ end
147
+ end
148
+
149
+ def self.rbx?
150
+ RbConfig::CONFIG['ruby_install_name'] == 'rbx'
151
+ end
152
+ end
153
+
154
+ extend self
155
+ end
156
+ end
@@ -1 +1 @@
1
- 0.1
1
+ 0.2
@@ -1,4 +1,5 @@
1
1
  require 'rbconfig'
2
+ require 'securerandom'
2
3
  require 'rack/utils'
3
4
  require 'pp'
4
5
 
@@ -23,47 +24,29 @@ class Introspector
23
24
  module Overloads
24
25
  end
25
26
 
26
- OVERLOAD.each do |m, object|
27
- if object.is_a? Array
28
- name = object.pop
29
- namespace = Object
30
-
31
- n = false
32
- object.each do |o|
33
- begin
34
- namespace = namespace.const_get( o )
35
- rescue
36
- n = true
37
- break
38
- end
39
- end
40
- next if n
41
-
42
- object = namespace.const_get( name ) rescue next
43
- else
44
- object = Object.const_get( object ) rescue next
45
- end
46
-
47
- overload( object, m )
48
- end
49
-
50
27
  @mutex = Mutex.new
51
28
  class <<self
52
29
  def overload( object, m )
30
+ method_source_location = object.allocate.method(m).source_location
31
+ rnd = SecureRandom.hex(10)
32
+
53
33
  ov = <<EORUBY
54
34
  module Overloads
55
- module #{object.to_s.split( '::' ).join}Overload
35
+ module #{object.to_s.split( '::' ).join}#{rnd}Overload
56
36
  def #{m}( *args )
57
- SCNR::Introspector.find_and_log_taint( #{object}, :#{m}, args )
37
+ SCNR::Introspector.find_and_log_taint( #{object}, :#{m}, #{method_source_location.inspect}, args )
58
38
  super *args
59
39
  end
60
40
  end
61
41
  end
62
42
 
63
- #{object}.prepend Overloads::#{object.to_s.split( '::' ).join}Overload
43
+ #{object}.prepend Overloads::#{object.to_s.split( '::' ).join}#{rnd}Overload
64
44
  EORUBY
65
45
  eval ov
66
- eval ov
46
+ rescue => e
47
+ # puts ov
48
+ # pp e
49
+ # pp e.backtrace
67
50
  end
68
51
 
69
52
  def taint_seed=( t )
@@ -95,7 +78,7 @@ EORUBY
95
78
  end
96
79
  end
97
80
 
98
- def find_and_log_taint( object, method, args )
81
+ def find_and_log_taint( object, method, method_source_location, args )
99
82
  taint = @taint
100
83
  return if !taint
101
84
 
@@ -108,7 +91,8 @@ EORUBY
108
91
  arguments: args,
109
92
  tainted_argument_index: tainted[0],
110
93
  tainted_value: tainted[1].to_s,
111
- backtrace: filter_caller( Kernel.caller )
94
+ backtrace: filter_caller( Kernel.caller[1..-1] ),
95
+ method_source_location: method_source_location
112
96
  )
113
97
  log_sinks( taint, sink )
114
98
  end
@@ -149,19 +133,65 @@ EORUBY
149
133
  end
150
134
  end
151
135
 
136
+ OVERLOAD.each do |m, object|
137
+ if object.is_a? Array
138
+ name = object.pop
139
+ namespace = Object
140
+
141
+ n = false
142
+ object.each do |o|
143
+ begin
144
+ namespace = namespace.const_get( o )
145
+ rescue
146
+ n = true
147
+ break
148
+ end
149
+ end
150
+ next if n
151
+
152
+ object = namespace.const_get( name ) rescue next
153
+ else
154
+ object = Object.const_get( object ) rescue next
155
+ end
156
+
157
+ overload( object, m )
158
+ end
159
+
152
160
  def initialize( app, options = {} )
153
161
  @app = app
154
162
  @options = options
155
163
 
156
164
  overload_application
165
+ overload_rails if rails?
157
166
 
158
167
  @mutex = Mutex.new
159
168
  end
160
169
 
161
170
  def overload_application
162
- @app.methods.each do |m|
163
- next if @app.method( m ).parameters.empty?
164
- self.class.overload( @app.class, m )
171
+ overload_class @app.class
172
+ end
173
+
174
+ def overload_rails
175
+ Rails.application.eager_load!
176
+
177
+ klasses = [
178
+ ActionController::Base,
179
+ ActiveRecord::Base
180
+ ]
181
+ descendants = klasses.map do |k|
182
+ ObjectSpace.each_object( Class ).select { |klass| klass < k }
183
+ end.flatten.reject { |k| k.to_s.start_with? '#' }
184
+
185
+ descendants.each do |klass|
186
+ overload_class klass
187
+ end
188
+ end
189
+
190
+ def overload_class( klass )
191
+ k = klass.allocate
192
+ k.methods.each do |m|
193
+ next if k.method( m ).parameters.empty?
194
+ self.class.overload( klass, m )
165
195
  end
166
196
  end
167
197
 
@@ -221,11 +251,14 @@ EORUBY
221
251
  code = response.shift
222
252
  headers = response.shift
223
253
  body = response.shift
254
+ body = body.respond_to?( :body ) ? body.body : body
224
255
 
256
+ body = [body].flatten
225
257
  body << "<!-- #{seed}\n#{JSON.dump( data )}\n#{seed} -->"
226
- headers['Content-Length'] = body.map(&:bytesize).inject(&:+)
227
258
 
228
- [code, headers, body ]
259
+ headers['Content-Length'] = body.map(&:bytesize).inject(:+)
260
+
261
+ [code, headers, [body].flatten ]
229
262
  end
230
263
 
231
264
  def platforms
@@ -282,9 +315,7 @@ EORUBY
282
315
  end
283
316
 
284
317
  def rails?
285
- if defined? Rails
286
- return @app.is_a? Rails::Application
287
- end
318
+ !!defined?( Rails )
288
319
  end
289
320
 
290
321
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scnr-introspector
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tasos Laskos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-21 00:00:00.000000000 Z
11
+ date: 2024-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,6 +100,7 @@ files:
100
100
  - lib/scnr/introspector/execution_flow/point.rb
101
101
  - lib/scnr/introspector/execution_flow/scope.rb
102
102
  - lib/scnr/introspector/scope.rb
103
+ - lib/scnr/introspector/utilities/my_method_source/code_helpers.rb
103
104
  - lib/scnr/introspector/version
104
105
  - lib/scnr/introspector/version.rb
105
106
  homepage: http://ecsypno.com