scnr-introspector 0.1
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 +7 -0
- data/bin/.gitkeep +0 -0
- data/lib/scnr/introspector/coverage/resource/line.rb +88 -0
- data/lib/scnr/introspector/coverage/resource.rb +90 -0
- data/lib/scnr/introspector/coverage.rb +102 -0
- data/lib/scnr/introspector/data_flow/scope.rb +11 -0
- data/lib/scnr/introspector/data_flow/sink.rb +54 -0
- data/lib/scnr/introspector/data_flow.rb +80 -0
- data/lib/scnr/introspector/error.rb +8 -0
- data/lib/scnr/introspector/execution_flow/point.rb +88 -0
- data/lib/scnr/introspector/execution_flow/scope.rb +11 -0
- data/lib/scnr/introspector/execution_flow.rb +105 -0
- data/lib/scnr/introspector/scope.rb +102 -0
- data/lib/scnr/introspector/version +1 -0
- data/lib/scnr/introspector/version.rb +5 -0
- data/lib/scnr/introspector.rb +291 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5682baf4ae51753f560e40d01c437b110fb798e9dda4e780799832e397177597
|
4
|
+
data.tar.gz: 6ccac3fd0802d210ae62105c97a27b407337df4a5460e556836201744f6f54c4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3256314a27a42c47c2e37b770d953827d487db5cb91f5fb01895a865bebfe7eff0078b435143575d56dbcc11d43accefcabc57ff35891d38d0631c96fc5f86b
|
7
|
+
data.tar.gz: cb0d31f2ad427fc23f04b13092dabf0d801badee3f4cedd41f06a1376535ad254055fca29f6c81ac2cf65ad1b3a696838497777108404c3fdd0d31154c1f2b3d
|
data/bin/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module SCNR
|
2
|
+
class Introspector
|
3
|
+
class Coverage
|
4
|
+
class Resource
|
5
|
+
|
6
|
+
class Line
|
7
|
+
|
8
|
+
# @return [Integer]
|
9
|
+
# Line number.
|
10
|
+
attr_accessor :number
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
# Line content.
|
14
|
+
attr_accessor :content
|
15
|
+
|
16
|
+
# @return [Resource]
|
17
|
+
# Resource containing `self`.
|
18
|
+
attr_accessor :resource
|
19
|
+
|
20
|
+
# @return [nil, Integer]
|
21
|
+
# Amount of times this line was executed:
|
22
|
+
#
|
23
|
+
# * `nil` -- {#skipped? Skipped}, irrelevant code.
|
24
|
+
# * `0` -- {#missed? Missed}, line wasn't executed.
|
25
|
+
# * `>= 1` -- {#hit? Hit}, line was executed.
|
26
|
+
attr_accessor :hits
|
27
|
+
|
28
|
+
# @param [Hash] options
|
29
|
+
# @option options [Resource] :resource
|
30
|
+
# @option options [Integer] :number
|
31
|
+
# @option options [String] :content
|
32
|
+
# @option options [nil, Integer] :his
|
33
|
+
def initialize( options = {} )
|
34
|
+
@resource = options[:resource]
|
35
|
+
fail ArgumentError, 'Missing :resource' if !@resource
|
36
|
+
|
37
|
+
@number = options[:number]
|
38
|
+
fail ArgumentError, 'Missing :number' if !@number.is_a?( Integer )
|
39
|
+
|
40
|
+
@content = options[:content]
|
41
|
+
fail ArgumentError, 'Missing :content' if !@content
|
42
|
+
|
43
|
+
@hits = options[:hits]
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Bool]
|
47
|
+
# `true` if the line is irrelevant to the coverage, `false` otherwise.
|
48
|
+
def skipped?
|
49
|
+
@hits.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Bool]
|
53
|
+
# `true` if the line wasn't executed, `false` otherwise.
|
54
|
+
def missed?
|
55
|
+
@hits == 0
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Bool]
|
59
|
+
# `true` if the line was executed, `false` otherwise.
|
60
|
+
def hit?
|
61
|
+
@hits.to_i > 0
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Symbol]
|
65
|
+
# * `:skipped` if {#skipped?}.
|
66
|
+
# * `:missed` if {#missed?}.
|
67
|
+
# * `:hit` if {#hit?}.
|
68
|
+
def state
|
69
|
+
[:skipped, :missed, :hit].each do |possible_state|
|
70
|
+
return possible_state if send("#{possible_state}?")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param [Integer] count
|
75
|
+
# Register `count` amount of hits.
|
76
|
+
def hit( count )
|
77
|
+
return if !count
|
78
|
+
|
79
|
+
@hits ||= 0
|
80
|
+
@hits += count
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'scnr/introspector/coverage/resource/line'
|
2
|
+
|
3
|
+
module SCNR
|
4
|
+
class Introspector
|
5
|
+
class Coverage
|
6
|
+
|
7
|
+
class Resource
|
8
|
+
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
# @return [Array<Line>]
|
13
|
+
attr_accessor :lines
|
14
|
+
|
15
|
+
# @param [String] path
|
16
|
+
# Path to the resource.
|
17
|
+
def initialize( path )
|
18
|
+
@path = path
|
19
|
+
@lines = []
|
20
|
+
|
21
|
+
IO.binread( path ).lines.each.with_index do |line, number|
|
22
|
+
@lines << Line.new(
|
23
|
+
number: number,
|
24
|
+
content: line.rstrip,
|
25
|
+
resource: self
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [Integer] line_number
|
31
|
+
# Number of the line to return (0-indexed).
|
32
|
+
#
|
33
|
+
# @return [Line]
|
34
|
+
def []( line_number )
|
35
|
+
@lines[line_number]
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Array<Line>]
|
39
|
+
# {Line#hit? Hit} lines.
|
40
|
+
def hit_lines
|
41
|
+
lines.select(&:hit?)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Array<Line>]
|
45
|
+
# {Line#missed? Missed} lines.
|
46
|
+
def missed_lines
|
47
|
+
lines.select(&:missed?)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array<Line>]
|
51
|
+
# {Line#skipped? Skipped} lines.
|
52
|
+
def skipped_lines
|
53
|
+
lines.select(&:skipped?)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Array<Line>]
|
57
|
+
# Lines which should be considered in coverage (i.e. all lines except for
|
58
|
+
# {#skipped_lines}).
|
59
|
+
def included_lines
|
60
|
+
lines - skipped_lines
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Float]
|
64
|
+
# Percentage of {#hit_line}s.
|
65
|
+
def hit_percentage
|
66
|
+
return 100.0 if empty?
|
67
|
+
|
68
|
+
lines_to_include = Float(lines.size - skipped_lines.size)
|
69
|
+
return 100.0 if lines_to_include == 0
|
70
|
+
|
71
|
+
(hit_lines.size / lines_to_include) * 100.0
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Float]
|
75
|
+
# Percentage of {#missed_line}s.
|
76
|
+
def miss_percentage
|
77
|
+
100.0 - hit_percentage
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Bool]
|
81
|
+
# `true` if {#lines} are empty, `false` otherwise.
|
82
|
+
def empty?
|
83
|
+
lines.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'coverage'
|
2
|
+
require 'scnr/introspector/coverage/resource'
|
3
|
+
|
4
|
+
module SCNR
|
5
|
+
class Introspector
|
6
|
+
class Coverage
|
7
|
+
|
8
|
+
class <<self
|
9
|
+
|
10
|
+
# @note Enabling scan coverage may cause segfaults depending on
|
11
|
+
# interpreter version and web application type.
|
12
|
+
#
|
13
|
+
# Enables coverage tracking for all subsequently required source files.
|
14
|
+
def enable
|
15
|
+
::Coverage.start
|
16
|
+
@enabled = true
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Bool]
|
20
|
+
# `true` if coverage has been {#enabled}, `false` otherwise.
|
21
|
+
def enabled?
|
22
|
+
!!@enabled
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Scope < Introspector::Scope
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Scope]
|
30
|
+
attr_accessor :scope
|
31
|
+
|
32
|
+
# @return [Hash<String, Resource>]
|
33
|
+
# All in-scope web application resources, per path.
|
34
|
+
attr_reader :resources
|
35
|
+
|
36
|
+
# @param [Hash] options
|
37
|
+
# @option options [Scope,Hash,nil] :scope
|
38
|
+
# * {Scope}: Configured {Scope} to use.
|
39
|
+
# * `Hash`: `Hash` to use for {Scope#initialize}.
|
40
|
+
# * `nil`: Will default to an empty {Scope}.
|
41
|
+
#
|
42
|
+
# @raise [Introspector::Scope::Error::Invalid]
|
43
|
+
# On unsupported `:scope` option.
|
44
|
+
def initialize( options = {} )
|
45
|
+
options = options.dup
|
46
|
+
|
47
|
+
if (scope = options.delete(:scope)).is_a? Scope
|
48
|
+
@scope = scope
|
49
|
+
elsif scope.is_a? Hash
|
50
|
+
@scope = Scope.new( scope )
|
51
|
+
elsif scope.nil?
|
52
|
+
@scope = Scope.new
|
53
|
+
else
|
54
|
+
fail Scope::Error::Invalid
|
55
|
+
end
|
56
|
+
|
57
|
+
@resources = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def retrieve_results
|
61
|
+
import_native( ::Coverage.result )
|
62
|
+
end
|
63
|
+
|
64
|
+
def import_native( coverage )
|
65
|
+
coverage.each do |path, lines|
|
66
|
+
next if @scope.out?( path )
|
67
|
+
|
68
|
+
@resources[path] ||= Resource.new( path )
|
69
|
+
|
70
|
+
lines.each.with_index do |hits, line_number|
|
71
|
+
@resources[path][line_number].hit( hits )
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Float]
|
79
|
+
# Percentage of coverage for all application resources which are within scope.
|
80
|
+
def percentage
|
81
|
+
return 100.0 if resources.empty?
|
82
|
+
|
83
|
+
total_coverages = 0
|
84
|
+
resources.each do |_, resource|
|
85
|
+
total_coverages += resource.hit_percentage
|
86
|
+
end
|
87
|
+
|
88
|
+
total_coverages / resources.size
|
89
|
+
end
|
90
|
+
|
91
|
+
def hash
|
92
|
+
[resources, scope].hash
|
93
|
+
end
|
94
|
+
|
95
|
+
def ==( other )
|
96
|
+
hash == other.hash
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SCNR
|
2
|
+
class Introspector
|
3
|
+
class DataFlow
|
4
|
+
|
5
|
+
class Sink
|
6
|
+
|
7
|
+
attr_accessor :object
|
8
|
+
|
9
|
+
attr_accessor :method_name
|
10
|
+
|
11
|
+
attr_accessor :arguments
|
12
|
+
|
13
|
+
attr_accessor :tainted_argument_index
|
14
|
+
|
15
|
+
attr_accessor :tainted_value
|
16
|
+
|
17
|
+
attr_accessor :backtrace
|
18
|
+
|
19
|
+
# @param [Hash] options
|
20
|
+
def initialize( options = {} )
|
21
|
+
options.each do |k, v|
|
22
|
+
next if v.nil?
|
23
|
+
|
24
|
+
send( "#{k}=", v )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def marshal_dump
|
29
|
+
instance_variables.inject( {} ) do |h, iv|
|
30
|
+
h[iv.to_s.gsub('@','')] = instance_variable_get( iv )
|
31
|
+
h
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_rpc_data
|
36
|
+
marshal_dump.merge 'arguments' => arguments.map(&:to_json)
|
37
|
+
end
|
38
|
+
|
39
|
+
def marshal_load( h )
|
40
|
+
h.each { |k, v| instance_variable_set( "@#{k}", v ) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_rpc_data( data )
|
44
|
+
n = self.new
|
45
|
+
n.marshal_load( data )
|
46
|
+
n.arguments = n.arguments.map { |a| ::JSON.load a }
|
47
|
+
n
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'scnr/introspector/data_flow/scope'
|
2
|
+
require 'scnr/introspector/data_flow/sink'
|
3
|
+
|
4
|
+
module SCNR
|
5
|
+
class Introspector
|
6
|
+
|
7
|
+
class DataFlow
|
8
|
+
|
9
|
+
# @return [Scope]
|
10
|
+
attr_accessor :scope
|
11
|
+
|
12
|
+
# @return [Array<Point>]
|
13
|
+
attr_reader :sinks
|
14
|
+
|
15
|
+
# @param [Hash] options
|
16
|
+
# @option options [Scope,Hash,nil] :scope
|
17
|
+
# * {Scope}: Configured {Scope} to use.
|
18
|
+
# * `Hash`: `Hash` to use for {Scope#initialize}.
|
19
|
+
# * `nil`: Will default to an empty {Scope}.
|
20
|
+
# @param [Block] block
|
21
|
+
# Code to {#trace}.
|
22
|
+
#
|
23
|
+
# @raise [Introspector::Scope::Error::Invalid]
|
24
|
+
# On unsupported `:scope` option.
|
25
|
+
def initialize( options = {}, &block )
|
26
|
+
options = options.dup
|
27
|
+
|
28
|
+
if (scope = options.delete(:scope)).is_a? DataFlow::Scope
|
29
|
+
@scope = scope
|
30
|
+
elsif scope.is_a? Hash
|
31
|
+
@scope = DataFlow::Scope.new( scope )
|
32
|
+
elsif scope.nil?
|
33
|
+
@scope = DataFlow::Scope.new
|
34
|
+
else
|
35
|
+
fail DataFlow::Scope::Error::Invalid
|
36
|
+
end
|
37
|
+
|
38
|
+
@sinks = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_rpc_data
|
42
|
+
data = {}
|
43
|
+
instance_variables.each do |iv|
|
44
|
+
case iv
|
45
|
+
when :@sinks
|
46
|
+
data['sinks'] = @sinks.map(&:to_rpc_data)
|
47
|
+
|
48
|
+
when :@scope
|
49
|
+
next
|
50
|
+
|
51
|
+
else
|
52
|
+
v = instance_variable_get( iv )
|
53
|
+
next if !v
|
54
|
+
data[iv.to_s.gsub('@','')] = v.to_rpc_data
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
data
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.from_rpc_data( h )
|
62
|
+
n = self.new
|
63
|
+
|
64
|
+
h.each do |k, v|
|
65
|
+
case k
|
66
|
+
when 'sinks'
|
67
|
+
n.instance_variable_set( "@#{k}", v.map { |pd| Sink.from_rpc_data( pd ) } )
|
68
|
+
|
69
|
+
else
|
70
|
+
n.instance_variable_set( "@#{k}", v )
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
n
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module SCNR
|
2
|
+
class Introspector
|
3
|
+
class ExecutionFlow
|
4
|
+
|
5
|
+
# Trace point, similar in function to a native Ruby TracePoint.
|
6
|
+
# Points to a code execution {#event}.
|
7
|
+
class Point
|
8
|
+
|
9
|
+
# @return [String,nil]
|
10
|
+
# Path to the source file, `nil` if no file is available (i.e. compiled code).
|
11
|
+
attr_accessor :path
|
12
|
+
|
13
|
+
# @return [Integer,nil]
|
14
|
+
# File line number, `nil` if no file is available (i.e. compiled code).
|
15
|
+
attr_accessor :line_number
|
16
|
+
|
17
|
+
# @return [String]
|
18
|
+
# Class name containing the point.
|
19
|
+
attr_accessor :class_name
|
20
|
+
|
21
|
+
# @return [Symbol]
|
22
|
+
# Name of method associated with the {#event}.
|
23
|
+
attr_accessor :method_name
|
24
|
+
|
25
|
+
# @return [Symbol]
|
26
|
+
# Event name.
|
27
|
+
attr_accessor :event
|
28
|
+
|
29
|
+
# @param [Hash] options
|
30
|
+
def initialize( options = {} )
|
31
|
+
options.each do |k, v|
|
32
|
+
next if v.nil?
|
33
|
+
|
34
|
+
send( "#{k}=", v )
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
"#{path}:#{line_number} #{class_name}##{method_name} #{event}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def marshal_dump
|
43
|
+
instance_variables.inject( {} ) do |h, iv|
|
44
|
+
h[iv.to_s.gsub('@','')] = instance_variable_get( iv )
|
45
|
+
h
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def marshal_load( h )
|
50
|
+
h.each { |k, v| instance_variable_set( "@#{k}", v ) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_rpc_data
|
54
|
+
marshal_dump
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.from_rpc_data( data )
|
58
|
+
n = self.new
|
59
|
+
n.marshal_load( data )
|
60
|
+
n
|
61
|
+
end
|
62
|
+
|
63
|
+
class <<self
|
64
|
+
|
65
|
+
# @param [TracePoint] tp
|
66
|
+
# Ruby TracePoint object.
|
67
|
+
#
|
68
|
+
# @return [Point]
|
69
|
+
def from_trace_point( tp )
|
70
|
+
defined_class =
|
71
|
+
(tp.defined_class.is_a?( Class ) || tp.defined_class.is_a?( Module ) ?
|
72
|
+
tp.defined_class.name : tp.defined_class.class.name)
|
73
|
+
|
74
|
+
new({
|
75
|
+
path: tp.path,
|
76
|
+
line_number: tp.lineno,
|
77
|
+
class_name: defined_class,
|
78
|
+
method_name: tp.method_id,
|
79
|
+
event: tp.event
|
80
|
+
})
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'scnr/introspector/execution_flow/scope'
|
2
|
+
require 'scnr/introspector/execution_flow/point'
|
3
|
+
|
4
|
+
module SCNR
|
5
|
+
class Introspector
|
6
|
+
|
7
|
+
class ExecutionFlow
|
8
|
+
|
9
|
+
# @return [Scope]
|
10
|
+
attr_accessor :scope
|
11
|
+
|
12
|
+
# @return [Array<Point>]
|
13
|
+
attr_reader :points
|
14
|
+
|
15
|
+
# @param [Hash] options
|
16
|
+
# @option options [Scope,Hash,nil] :scope
|
17
|
+
# * {Scope}: Configured {Scope} to use.
|
18
|
+
# * `Hash`: `Hash` to use for {Scope#initialize}.
|
19
|
+
# * `nil`: Will default to an empty {Scope}.
|
20
|
+
# @param [Block] block
|
21
|
+
# Code to {#trace}.
|
22
|
+
#
|
23
|
+
# @raise [Introspector::Scope::Error::Invalid]
|
24
|
+
# On unsupported `:scope` option.
|
25
|
+
def initialize( options = {}, &block )
|
26
|
+
options = options.dup
|
27
|
+
|
28
|
+
if (scope = options.delete(:scope)).is_a? ExecutionFlow::Scope
|
29
|
+
@scope = scope
|
30
|
+
elsif scope.is_a? Hash
|
31
|
+
@scope = ExecutionFlow::Scope.new( scope )
|
32
|
+
elsif scope.nil?
|
33
|
+
@scope = ExecutionFlow::Scope.new
|
34
|
+
else
|
35
|
+
fail ExecutionFlow::Scope::Error::Invalid
|
36
|
+
end
|
37
|
+
|
38
|
+
@points = []
|
39
|
+
|
40
|
+
trace( &block ) if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Traces code execution events as {Point points} and populates {#points}.
|
44
|
+
#
|
45
|
+
# @param [Block] block
|
46
|
+
# Code to trace.
|
47
|
+
#
|
48
|
+
# @return [ExecutionFlow]
|
49
|
+
# `self`
|
50
|
+
def trace( &block )
|
51
|
+
TracePoint.new do |tp|
|
52
|
+
next if @scope.out?( tp.path )
|
53
|
+
|
54
|
+
@points << create_point_from_trace_point( tp )
|
55
|
+
end.enable(&block)
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_rpc_data
|
61
|
+
data = {}
|
62
|
+
instance_variables.each do |iv|
|
63
|
+
case iv
|
64
|
+
when :@points
|
65
|
+
data['points'] = @points.map(&:to_rpc_data)
|
66
|
+
|
67
|
+
when :@scope
|
68
|
+
next
|
69
|
+
|
70
|
+
else
|
71
|
+
v = instance_variable_get( iv )
|
72
|
+
next if !v
|
73
|
+
data[iv.to_s.gsub('@','')] = v.to_rpc_data
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
data
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.from_rpc_data( h )
|
81
|
+
n = self.new
|
82
|
+
|
83
|
+
h.each do |k, v|
|
84
|
+
case k
|
85
|
+
when 'points'
|
86
|
+
n.instance_variable_set( "@#{k}", v.map { |pd| Point.from_rpc_data( pd ) } )
|
87
|
+
|
88
|
+
else
|
89
|
+
n.instance_variable_set( "@#{k}", v )
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
n
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def create_point_from_trace_point( tp )
|
99
|
+
Point.from_trace_point( tp )
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module SCNR
|
2
|
+
class Introspector
|
3
|
+
class Scope
|
4
|
+
|
5
|
+
class Error < Introspector::Error
|
6
|
+
class Invalid < Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class UnknownOption < Error
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String,nil]
|
14
|
+
# Include trace points whose file path starts with this string.
|
15
|
+
attr_accessor :path_start_with
|
16
|
+
|
17
|
+
# @return [String,nil]
|
18
|
+
# Include trace points whose file path ends with this string.
|
19
|
+
attr_accessor :path_end_with
|
20
|
+
|
21
|
+
# @return [Array<Regexp>]
|
22
|
+
# Include trace points whose file path matches this pattern.
|
23
|
+
attr_accessor :path_include_patterns
|
24
|
+
|
25
|
+
# @return [Array<Regexp>]
|
26
|
+
# Exclude trace points whose file path matches this pattern.
|
27
|
+
attr_accessor :path_exclude_patterns
|
28
|
+
|
29
|
+
# @param [Hash] options
|
30
|
+
# Sets instance attributes.
|
31
|
+
def initialize( options = {} )
|
32
|
+
options.each do |k, v|
|
33
|
+
begin
|
34
|
+
send( "#{k}=", v )
|
35
|
+
rescue NoMethodError
|
36
|
+
fail "Unknown option: #{k}", Error::UnknownOption
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@path_include_patterns ||= []
|
41
|
+
@path_exclude_patterns ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Bool]
|
45
|
+
# `true` if scope has no configuration, `false` otherwise.
|
46
|
+
def empty?
|
47
|
+
!@path_start_with && !@path_end_with && @path_include_patterns.empty? &&
|
48
|
+
@path_exclude_patterns.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [String] path
|
52
|
+
# Path to check.
|
53
|
+
#
|
54
|
+
# @return [Bool]
|
55
|
+
# `true` if `path` is not `#in?` scope, `false` otherwise.
|
56
|
+
def out?( path )
|
57
|
+
!in?( path )
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [String] path
|
61
|
+
# Path to check.
|
62
|
+
#
|
63
|
+
# @return [Bool]
|
64
|
+
# `true` if `path` is `#in?` scope, `false` otherwise.
|
65
|
+
def in?( path )
|
66
|
+
if @path_start_with
|
67
|
+
return path.to_s.start_with?( @path_start_with )
|
68
|
+
end
|
69
|
+
|
70
|
+
if @path_end_with
|
71
|
+
return path.to_s.end_with?( @path_end_with )
|
72
|
+
end
|
73
|
+
|
74
|
+
if @path_include_patterns.any?
|
75
|
+
@path_include_patterns.each do |pattern|
|
76
|
+
return true if path =~ pattern
|
77
|
+
end
|
78
|
+
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
|
82
|
+
if @path_exclude_patterns.any?
|
83
|
+
@path_exclude_patterns.each do |pattern|
|
84
|
+
return false if path =~ pattern
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
def hash
|
92
|
+
[@path_start_with, @path_end_with, @path_include_patterns, @path_exclude_patterns].hash
|
93
|
+
end
|
94
|
+
|
95
|
+
def ==( other )
|
96
|
+
hash == other.hash
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
0.1
|
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
module SCNR
|
6
|
+
class Introspector
|
7
|
+
include Rack::Utils
|
8
|
+
|
9
|
+
require 'scnr/introspector/version'
|
10
|
+
require 'scnr/introspector/error'
|
11
|
+
require 'scnr/introspector/scope'
|
12
|
+
require 'scnr/introspector/execution_flow'
|
13
|
+
require 'scnr/introspector/data_flow'
|
14
|
+
require 'scnr/introspector/coverage'
|
15
|
+
|
16
|
+
# Coverage.enable
|
17
|
+
|
18
|
+
OVERLOAD = [
|
19
|
+
[:erb, :Templates],
|
20
|
+
[:test, [:SCNR, :Introspector, :Test]]
|
21
|
+
]
|
22
|
+
|
23
|
+
module Overloads
|
24
|
+
end
|
25
|
+
|
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
|
+
@mutex = Mutex.new
|
51
|
+
class <<self
|
52
|
+
def overload( object, m )
|
53
|
+
ov = <<EORUBY
|
54
|
+
module Overloads
|
55
|
+
module #{object.to_s.split( '::' ).join}Overload
|
56
|
+
def #{m}( *args )
|
57
|
+
SCNR::Introspector.find_and_log_taint( #{object}, :#{m}, args )
|
58
|
+
super *args
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#{object}.prepend Overloads::#{object.to_s.split( '::' ).join}Overload
|
64
|
+
EORUBY
|
65
|
+
eval ov
|
66
|
+
eval ov
|
67
|
+
end
|
68
|
+
|
69
|
+
def taint_seed=( t )
|
70
|
+
@taint = t
|
71
|
+
end
|
72
|
+
|
73
|
+
def taint_seed
|
74
|
+
@taint
|
75
|
+
end
|
76
|
+
|
77
|
+
def data_flows
|
78
|
+
@data_flows ||= {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def synchronize( &block )
|
82
|
+
@mutex.synchronize( &block )
|
83
|
+
end
|
84
|
+
|
85
|
+
def log_sinks( taint, sink )
|
86
|
+
synchronize do
|
87
|
+
(self.data_flows[taint] ||= DataFlow.new).sinks << sink
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def filter_caller( a )
|
92
|
+
dir = File.dirname( __FILE__ )
|
93
|
+
a.reject do |c|
|
94
|
+
c.start_with?( dir ) || c.include?( 'trace_point' )
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_and_log_taint( object, method, args )
|
99
|
+
taint = @taint
|
100
|
+
return if !taint
|
101
|
+
|
102
|
+
tainted = find_taint_in_arguments( taint, args )
|
103
|
+
return if !tainted
|
104
|
+
|
105
|
+
sink = DataFlow::Sink.new(
|
106
|
+
object: object.to_s,
|
107
|
+
method_name: method.to_s,
|
108
|
+
arguments: args,
|
109
|
+
tainted_argument_index: tainted[0],
|
110
|
+
tainted_value: tainted[1].to_s,
|
111
|
+
backtrace: filter_caller( Kernel.caller )
|
112
|
+
)
|
113
|
+
log_sinks( taint, sink )
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_taint_in_arguments( taint, args )
|
117
|
+
args.each.with_index do |arg, i|
|
118
|
+
value = find_taint_recursively( taint, arg, i )
|
119
|
+
next if !value
|
120
|
+
|
121
|
+
return [i, value]
|
122
|
+
end
|
123
|
+
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def find_taint_recursively( taint, object, depth )
|
128
|
+
case object
|
129
|
+
when Hash
|
130
|
+
object.each do |k, v|
|
131
|
+
t = find_taint_recursively( taint, v, depth )
|
132
|
+
return t if t
|
133
|
+
end
|
134
|
+
|
135
|
+
when Array
|
136
|
+
object.each do |v|
|
137
|
+
t = find_taint_recursively( taint, v, depth )
|
138
|
+
return t if t
|
139
|
+
end
|
140
|
+
|
141
|
+
when String
|
142
|
+
return object if object.include? taint
|
143
|
+
|
144
|
+
else
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def initialize( app, options = {} )
|
153
|
+
@app = app
|
154
|
+
@options = options
|
155
|
+
|
156
|
+
overload_application
|
157
|
+
|
158
|
+
@mutex = Mutex.new
|
159
|
+
end
|
160
|
+
|
161
|
+
def overload_application
|
162
|
+
@app.methods.each do |m|
|
163
|
+
next if @app.method( m ).parameters.empty?
|
164
|
+
self.class.overload( @app.class, m )
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def synchronize( &block )
|
169
|
+
@mutex.synchronize( &block )
|
170
|
+
end
|
171
|
+
|
172
|
+
def call( env )
|
173
|
+
info = Set.new
|
174
|
+
info << :platforms
|
175
|
+
|
176
|
+
if env.delete( 'HTTP_X_SCNR_INTROSPECTOR_TRACE' )
|
177
|
+
info << :data_flow
|
178
|
+
info << :execution_flow
|
179
|
+
end
|
180
|
+
|
181
|
+
inject( env, info )
|
182
|
+
|
183
|
+
rescue => e
|
184
|
+
pp e
|
185
|
+
pp e.backtrace
|
186
|
+
end
|
187
|
+
|
188
|
+
def inject( env, info = [] )
|
189
|
+
self.class.taint_seed = env.delete( 'HTTP_X_SCNR_INTROSPECTOR_TAINT' )
|
190
|
+
seed = env.delete( 'HTTP_X_SCNR_ENGINE_SCAN_SEED' )
|
191
|
+
|
192
|
+
data = {}
|
193
|
+
|
194
|
+
response = nil
|
195
|
+
if info.include? :execution_flow
|
196
|
+
|
197
|
+
execution_flow = nil
|
198
|
+
synchronize do
|
199
|
+
execution_flow = ExecutionFlow.new @options do
|
200
|
+
response = @app.call( env )
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
data['execution_flow'] = execution_flow.to_rpc_data
|
205
|
+
else
|
206
|
+
response = @app.call( env )
|
207
|
+
end
|
208
|
+
|
209
|
+
if info.include? :platforms
|
210
|
+
data['platforms'] = self.platforms
|
211
|
+
end
|
212
|
+
|
213
|
+
if info.include?( :coverage ) && Coverage.enabled?
|
214
|
+
data['coverage'] = Coverage.new( @options ).retrieve_results
|
215
|
+
end
|
216
|
+
|
217
|
+
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
|
219
|
+
end
|
220
|
+
|
221
|
+
code = response.shift
|
222
|
+
headers = response.shift
|
223
|
+
body = response.shift
|
224
|
+
|
225
|
+
body << "<!-- #{seed}\n#{JSON.dump( data )}\n#{seed} -->"
|
226
|
+
headers['Content-Length'] = body.map(&:bytesize).inject(&:+)
|
227
|
+
|
228
|
+
[code, headers, body ]
|
229
|
+
end
|
230
|
+
|
231
|
+
def platforms
|
232
|
+
platforms = [:ruby, os, db]
|
233
|
+
if rails?
|
234
|
+
platforms << :rails
|
235
|
+
end
|
236
|
+
platforms.compact
|
237
|
+
end
|
238
|
+
|
239
|
+
# @return [Symbol]
|
240
|
+
# {SCNR::Platform::Manager::OS OS platform type} to use for
|
241
|
+
# {SCNR::Options#platforms}.
|
242
|
+
def os
|
243
|
+
@os ||= (
|
244
|
+
host_os = RbConfig::CONFIG['host_os']
|
245
|
+
|
246
|
+
case host_os
|
247
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
248
|
+
:windows
|
249
|
+
|
250
|
+
when /linux/
|
251
|
+
:linux
|
252
|
+
|
253
|
+
when /darwin|mac os|bsd/
|
254
|
+
:bsd
|
255
|
+
|
256
|
+
when /solaris/
|
257
|
+
:solaris
|
258
|
+
|
259
|
+
else
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
)
|
263
|
+
end
|
264
|
+
|
265
|
+
def db
|
266
|
+
return if !rails?
|
267
|
+
|
268
|
+
case ActiveRecord::Base.connection.adapter_name
|
269
|
+
when 'PostgreSQL'
|
270
|
+
:pgsql
|
271
|
+
|
272
|
+
when 'MySQL'
|
273
|
+
:mysql
|
274
|
+
|
275
|
+
when 'SQLite3'
|
276
|
+
:sqlite
|
277
|
+
|
278
|
+
else
|
279
|
+
nil
|
280
|
+
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def rails?
|
285
|
+
if defined? Rails
|
286
|
+
return @app.is_a? Rails::Application
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scnr-introspector
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tasos Laskos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: puma
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sinatra
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra-contrib
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- tasos.laskos@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- bin/.gitkeep
|
91
|
+
- lib/scnr/introspector.rb
|
92
|
+
- lib/scnr/introspector/coverage.rb
|
93
|
+
- lib/scnr/introspector/coverage/resource.rb
|
94
|
+
- lib/scnr/introspector/coverage/resource/line.rb
|
95
|
+
- lib/scnr/introspector/data_flow.rb
|
96
|
+
- lib/scnr/introspector/data_flow/scope.rb
|
97
|
+
- lib/scnr/introspector/data_flow/sink.rb
|
98
|
+
- lib/scnr/introspector/error.rb
|
99
|
+
- lib/scnr/introspector/execution_flow.rb
|
100
|
+
- lib/scnr/introspector/execution_flow/point.rb
|
101
|
+
- lib/scnr/introspector/execution_flow/scope.rb
|
102
|
+
- lib/scnr/introspector/scope.rb
|
103
|
+
- lib/scnr/introspector/version
|
104
|
+
- lib/scnr/introspector/version.rb
|
105
|
+
homepage: http://ecsypno.com
|
106
|
+
licenses:
|
107
|
+
- Commercial
|
108
|
+
metadata: {}
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubygems_version: 3.4.22
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Rack application security scanner built around the SCNR::Engine.
|
128
|
+
test_files: []
|