scnr-introspector 0.1 → 0.3.0

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: 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