scnr-introspector 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []