scnr-introspector 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
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