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 +4 -4
- data/lib/scnr/introspector/data_flow/sink.rb +27 -0
- data/lib/scnr/introspector/execution_flow/point.rb +21 -1
- data/lib/scnr/introspector/execution_flow.rb +3 -0
- data/lib/scnr/introspector/utilities/my_method_source/code_helpers.rb +156 -0
- data/lib/scnr/introspector/version +1 -1
- data/lib/scnr/introspector.rb +69 -38
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '06581df63125568c3bcd2c6f996bc81e20426d750397806e47ccf7d31a6a6ff2'
|
4
|
+
data.tar.gz: 814cd55b04084b83615ffcd3433b336ee5ffd3e5b65539b0bcdd7d75d9e16c8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
0.2
|
data/lib/scnr/introspector.rb
CHANGED
@@ -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
|
-
|
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.
|
163
|
-
|
164
|
-
|
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
|
-
[
|
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
|
-
|
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.
|
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-
|
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
|