scnr-introspector 0.1 → 0.3.0

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: 8725bd56ebb19d4dc43d75c4c19a2fde62996e6b0df84ac074f7baf5049f95db
4
+ data.tar.gz: b5b35b5cd86bbb75554b2ba8b144f87606b17990c90351df37417f3ec79cd3f2
5
5
  SHA512:
6
- metadata.gz: b3256314a27a42c47c2e37b770d953827d487db5cb91f5fb01895a865bebfe7eff0078b435143575d56dbcc11d43accefcabc57ff35891d38d0631c96fc5f86b
7
- data.tar.gz: cb0d31f2ad427fc23f04b13092dabf0d801badee3f4cedd41f06a1376535ad254055fca29f6c81ac2cf65ad1b3a696838497777108404c3fdd0d31154c1f2b3d
6
+ metadata.gz: 2ce2e0fa5215945851e1249a00f258a9ad542e090fe5f64402d2f804e1944f472c46438468a86b3fbf6cbb7d2e4aa2a092b89f5a7d50a90b92007b9a1bd1ca88
7
+ data.tar.gz: 5f281728c6423aa8222eb3b362dbac6a5715c60361a8d52666ea37a9b075542bac4fa4342e3b26e2aff80bb57bedaa7da44c8eb57ffc8e56cfb26467c3018b6d
@@ -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,9 @@ class Point
26
28
  # Event name.
27
29
  attr_accessor :event
28
30
 
31
+ attr_accessor :source
32
+ attr_accessor :file_contents
33
+
29
34
  # @param [Hash] options
30
35
  def initialize( options = {} )
31
36
  options.each do |k, v|
@@ -76,10 +81,27 @@ class Point
76
81
  line_number: tp.lineno,
77
82
  class_name: defined_class,
78
83
  method_name: tp.method_id,
79
- event: tp.event
84
+ event: tp.event,
85
+ source: source_line( tp.path, tp.lineno ),
86
+ file_contents: IO.read( tp.path )
80
87
  })
81
88
  end
89
+
90
+ def source_line_mutex( &block )
91
+ (@mutex ||= Mutex.new).synchronize( &block )
92
+ end
93
+
94
+ def source_line( path, line )
95
+ return if !path || !line
96
+
97
+ source_line_mutex do
98
+ @@lines ||= {}
99
+ @@lines[path] ||= IO.readlines( path )
100
+ @@lines[path][line-1]
101
+ end
102
+ end
82
103
  end
104
+ source_line_mutex {}
83
105
 
84
106
  end
85
107
 
@@ -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.3.0
@@ -1,5 +1,7 @@
1
1
  require 'rbconfig'
2
+ require 'securerandom'
2
3
  require 'rack/utils'
4
+ require 'base64'
3
5
  require 'pp'
4
6
 
5
7
  module SCNR
@@ -23,59 +25,41 @@ class Introspector
23
25
  module Overloads
24
26
  end
25
27
 
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
28
  @mutex = Mutex.new
51
29
  class <<self
52
30
  def overload( object, m )
31
+ method_source_location = object.allocate.method(m).source_location
32
+ rnd = SecureRandom.hex(10)
33
+
53
34
  ov = <<EORUBY
54
35
  module Overloads
55
- module #{object.to_s.split( '::' ).join}Overload
36
+ module #{object.to_s.split( '::' ).join}#{rnd}Overload
56
37
  def #{m}( *args )
57
- SCNR::Introspector.find_and_log_taint( #{object}, :#{m}, args )
38
+ SCNR::Introspector.find_and_log_taint( #{object}, :#{m}, #{method_source_location.inspect}, args )
58
39
  super *args
59
40
  end
60
41
  end
61
42
  end
62
43
 
63
- #{object}.prepend Overloads::#{object.to_s.split( '::' ).join}Overload
44
+ #{object}.prepend Overloads::#{object.to_s.split( '::' ).join}#{rnd}Overload
64
45
  EORUBY
65
46
  eval ov
66
- eval ov
47
+ rescue => e
48
+ # puts ov
49
+ # pp e
50
+ # pp e.backtrace
67
51
  end
68
52
 
69
53
  def taint_seed=( t )
70
- @taint = t
54
+ Thread.current[:taint] = t
71
55
  end
72
56
 
73
57
  def taint_seed
74
- @taint
58
+ Thread.current[:taint]
75
59
  end
76
60
 
77
61
  def data_flows
78
- @data_flows ||= {}
62
+ Thread.current[:data_flows] ||= {}
79
63
  end
80
64
 
81
65
  def synchronize( &block )
@@ -88,6 +72,12 @@ EORUBY
88
72
  end
89
73
  end
90
74
 
75
+ def flush_sinks( taint )
76
+ synchronize do
77
+ self.data_flows.delete taint
78
+ end
79
+ end
80
+
91
81
  def filter_caller( a )
92
82
  dir = File.dirname( __FILE__ )
93
83
  a.reject do |c|
@@ -95,8 +85,8 @@ EORUBY
95
85
  end
96
86
  end
97
87
 
98
- def find_and_log_taint( object, method, args )
99
- taint = @taint
88
+ def find_and_log_taint( object, method, method_source_location, args )
89
+ taint = self.taint_seed
100
90
  return if !taint
101
91
 
102
92
  tainted = find_taint_in_arguments( taint, args )
@@ -108,7 +98,8 @@ EORUBY
108
98
  arguments: args,
109
99
  tainted_argument_index: tainted[0],
110
100
  tainted_value: tainted[1].to_s,
111
- backtrace: filter_caller( Kernel.caller )
101
+ backtrace: filter_caller( Kernel.caller[1..-1] ),
102
+ method_source_location: method_source_location
112
103
  )
113
104
  log_sinks( taint, sink )
114
105
  end
@@ -149,19 +140,65 @@ EORUBY
149
140
  end
150
141
  end
151
142
 
143
+ OVERLOAD.each do |m, object|
144
+ if object.is_a? Array
145
+ name = object.pop
146
+ namespace = Object
147
+
148
+ n = false
149
+ object.each do |o|
150
+ begin
151
+ namespace = namespace.const_get( o )
152
+ rescue
153
+ n = true
154
+ break
155
+ end
156
+ end
157
+ next if n
158
+
159
+ object = namespace.const_get( name ) rescue next
160
+ else
161
+ object = Object.const_get( object ) rescue next
162
+ end
163
+
164
+ overload( object, m )
165
+ end
166
+
152
167
  def initialize( app, options = {} )
153
168
  @app = app
154
169
  @options = options
155
170
 
156
171
  overload_application
172
+ overload_rails if rails?
157
173
 
158
174
  @mutex = Mutex.new
159
175
  end
160
176
 
161
177
  def overload_application
162
- @app.methods.each do |m|
163
- next if @app.method( m ).parameters.empty?
164
- self.class.overload( @app.class, m )
178
+ overload_class @app.class
179
+ end
180
+
181
+ def overload_rails
182
+ Rails.application.eager_load!
183
+
184
+ klasses = [
185
+ ActionController::Base,
186
+ ActiveRecord::Base
187
+ ]
188
+ descendants = klasses.map do |k|
189
+ ObjectSpace.each_object( Class ).select { |klass| klass < k }
190
+ end.flatten.reject { |k| k.to_s.start_with? '#' }
191
+
192
+ descendants.each do |klass|
193
+ overload_class klass
194
+ end
195
+ end
196
+
197
+ def overload_class( klass )
198
+ k = klass.allocate
199
+ k.methods.each do |m|
200
+ next if k.method( m ).parameters.empty?
201
+ self.class.overload( klass, m )
165
202
  end
166
203
  end
167
204
 
@@ -174,10 +211,13 @@ EORUBY
174
211
  info << :platforms
175
212
 
176
213
  if env.delete( 'HTTP_X_SCNR_INTROSPECTOR_TRACE' )
177
- info << :data_flow
178
214
  info << :execution_flow
179
215
  end
180
216
 
217
+ if env['HTTP_X_SCNR_INTROSPECTOR_TAINT']
218
+ info << :data_flow
219
+ end
220
+
181
221
  inject( env, info )
182
222
 
183
223
  rescue => e
@@ -187,7 +227,12 @@ EORUBY
187
227
 
188
228
  def inject( env, info = [] )
189
229
  self.class.taint_seed = env.delete( 'HTTP_X_SCNR_INTROSPECTOR_TAINT' )
190
- seed = env.delete( 'HTTP_X_SCNR_ENGINE_SCAN_SEED' )
230
+ if self.class.taint_seed
231
+ self.class.taint_seed = Base64.decode64( self.class.taint_seed )
232
+ self.class.taint_seed = nil if self.class.taint_seed.empty?
233
+ end
234
+
235
+ seed = env.delete( 'HTTP_X_SCNR_ENGINE_SCAN_SEED' )
191
236
 
192
237
  data = {}
193
238
 
@@ -215,17 +260,25 @@ EORUBY
215
260
  end
216
261
 
217
262
  if info.include?( :data_flow ) && self.class.taint_seed
218
- data['data_flow'] = self.class.data_flows.delete( self.class.taint_seed )&.to_rpc_data
263
+ data['data_flow'] = self.class.flush_sinks( self.class.taint_seed )&.to_rpc_data
219
264
  end
220
265
 
221
266
  code = response.shift
222
267
  headers = response.shift
223
268
  body = response.shift
224
269
 
225
- body << "<!-- #{seed}\n#{JSON.dump( data )}\n#{seed} -->"
226
- headers['Content-Length'] = body.map(&:bytesize).inject(&:+)
270
+ if headers['Content-Type'] && headers['Content-Type'].include?( 'html' )
271
+ body = body.respond_to?( :body ) ? body.body : body
272
+ body = [body].flatten
273
+ body << "<!-- #{seed}\n#{JSON.dump( data )}\n#{seed} -->"
274
+
275
+ headers['Content-Length'] = body.map(&:bytesize).inject(:+)
276
+ end
227
277
 
228
- [code, headers, body ]
278
+ [code, headers, [body].flatten ]
279
+ rescue => e
280
+ pp e
281
+ pp e.backtrace
229
282
  end
230
283
 
231
284
  def platforms
@@ -282,9 +335,7 @@ EORUBY
282
335
  end
283
336
 
284
337
  def rails?
285
- if defined? Rails
286
- return @app.is_a? Rails::Application
287
- end
338
+ !!defined?( Rails )
288
339
  end
289
340
 
290
341
  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.3.0
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: 2025-01-01 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