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 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,11 @@
1
+ module SCNR
2
+ class Introspector
3
+ class DataFlow
4
+
5
+ class Scope < Introspector::Scope
6
+ end
7
+
8
+ end
9
+ end
10
+
11
+ 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,8 @@
1
+ module SCNR
2
+ class Introspector
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ end
8
+ 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,11 @@
1
+ module SCNR
2
+ class Introspector
3
+ class ExecutionFlow
4
+
5
+ class Scope < Introspector::Scope
6
+ end
7
+
8
+ end
9
+ end
10
+
11
+ 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,5 @@
1
+ module SCNR
2
+ class Introspector
3
+ VERSION = IO.read( File.dirname( __FILE__ ) + '/version' ).strip
4
+ end
5
+ end
@@ -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: []