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