contrast-agent 4.9.1 → 4.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec_parallel +6 -0
  4. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  5. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  6. data/ext/cs__common/cs__common.c +24 -7
  7. data/ext/cs__common/cs__common.h +12 -2
  8. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -12
  9. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -4
  10. data/ext/cs__os_information/cs__os_information.c +31 -0
  11. data/ext/cs__os_information/cs__os_information.h +7 -0
  12. data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
  13. data/lib/contrast/agent/assess/contrast_event.rb +1 -2
  14. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  15. data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
  16. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  17. data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
  18. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
  19. data/lib/contrast/agent/assess/policy/preshift.rb +29 -12
  20. data/lib/contrast/agent/assess/policy/propagation_method.rb +71 -142
  21. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  22. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -2
  23. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  24. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  25. data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
  26. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
  27. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
  28. data/lib/contrast/agent/assess/policy/source_method.rb +15 -88
  29. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
  30. data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -172
  31. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  32. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  33. data/lib/contrast/agent/assess/property/tagged.rb +15 -132
  34. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
  35. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  36. data/lib/contrast/agent/disable_reaction.rb +1 -1
  37. data/lib/contrast/agent/exclusion_matcher.rb +0 -4
  38. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  39. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +7 -5
  40. data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
  41. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  42. data/lib/contrast/agent/middleware.rb +23 -0
  43. data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
  44. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +17 -12
  45. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  46. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  47. data/lib/contrast/agent/patching/policy/patch.rb +42 -238
  48. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  49. data/lib/contrast/agent/patching/policy/patcher.rb +10 -49
  50. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  51. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  52. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  53. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  54. data/lib/contrast/agent/reaction_processor.rb +1 -1
  55. data/lib/contrast/agent/request.rb +9 -4
  56. data/lib/contrast/agent/request_context.rb +51 -33
  57. data/lib/contrast/agent/request_handler.rb +7 -3
  58. data/lib/contrast/agent/rule_set.rb +2 -4
  59. data/lib/contrast/agent/scope.rb +32 -20
  60. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  61. data/lib/contrast/agent/static_analysis.rb +5 -3
  62. data/lib/contrast/agent/telemetry.rb +129 -0
  63. data/lib/contrast/agent/telemetry_event.rb +34 -0
  64. data/lib/contrast/agent/thread_watcher.rb +43 -14
  65. data/lib/contrast/agent/tracepoint_hook.rb +16 -3
  66. data/lib/contrast/agent/version.rb +1 -1
  67. data/lib/contrast/agent.rb +6 -1
  68. data/lib/contrast/api/communication/messaging_queue.rb +12 -6
  69. data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
  70. data/lib/contrast/api/communication/socket_client.rb +4 -4
  71. data/lib/contrast/api/decorators/agent_startup.rb +4 -4
  72. data/lib/contrast/api/decorators/application_startup.rb +6 -5
  73. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  74. data/lib/contrast/components/agent.rb +5 -2
  75. data/lib/contrast/components/api.rb +34 -0
  76. data/lib/contrast/components/app_context.rb +24 -0
  77. data/lib/contrast/components/assess.rb +13 -3
  78. data/lib/contrast/components/base.rb +2 -2
  79. data/lib/contrast/components/config.rb +91 -11
  80. data/lib/contrast/components/contrast_service.rb +10 -2
  81. data/lib/contrast/components/logger.rb +13 -8
  82. data/lib/contrast/components/scope.rb +9 -28
  83. data/lib/contrast/config/api_configuration.rb +22 -0
  84. data/lib/contrast/config/assess_configuration.rb +1 -0
  85. data/lib/contrast/config/base_configuration.rb +14 -6
  86. data/lib/contrast/config/env_variables.rb +25 -0
  87. data/lib/contrast/config/root_configuration.rb +1 -0
  88. data/lib/contrast/config/service_configuration.rb +2 -1
  89. data/lib/contrast/config.rb +1 -0
  90. data/lib/contrast/configuration.rb +22 -15
  91. data/lib/contrast/extension/assess/array.rb +1 -11
  92. data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
  93. data/lib/contrast/extension/assess/fiber.rb +0 -11
  94. data/lib/contrast/extension/assess/hash.rb +0 -10
  95. data/lib/contrast/extension/assess/kernel.rb +1 -10
  96. data/lib/contrast/extension/assess/marshal.rb +3 -11
  97. data/lib/contrast/extension/assess/regexp.rb +0 -11
  98. data/lib/contrast/extension/assess/string.rb +1 -26
  99. data/lib/contrast/extension/extension.rb +61 -0
  100. data/lib/contrast/framework/grape/support.rb +174 -0
  101. data/lib/contrast/framework/manager.rb +56 -18
  102. data/lib/contrast/framework/rack/support.rb +1 -1
  103. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  104. data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
  105. data/lib/contrast/framework/rails/patch/support.rb +35 -30
  106. data/lib/contrast/framework/rails/railtie.rb +1 -1
  107. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
  108. data/lib/contrast/framework/rails/support.rb +60 -13
  109. data/lib/contrast/framework/sinatra/support.rb +1 -1
  110. data/lib/contrast/logger/application.rb +4 -0
  111. data/lib/contrast/logger/log.rb +89 -15
  112. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  113. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  114. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  115. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  116. data/lib/contrast/utils/class_util.rb +58 -44
  117. data/lib/contrast/utils/exclude_key.rb +20 -0
  118. data/lib/contrast/utils/io_util.rb +43 -35
  119. data/lib/contrast/utils/lru_cache.rb +45 -0
  120. data/lib/contrast/utils/metrics_hash.rb +59 -0
  121. data/lib/contrast/utils/os.rb +23 -0
  122. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  123. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  124. data/lib/contrast/utils/requests_client.rb +150 -0
  125. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  126. data/lib/contrast/utils/tag_util.rb +2 -1
  127. data/lib/contrast/utils/telemetry.rb +78 -0
  128. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  129. data/lib/contrast.rb +19 -1
  130. data/resources/assess/policy.json +208 -7
  131. data/resources/deadzone/policy.json +91 -0
  132. data/ruby-agent.gemspec +12 -2
  133. data/service_executables/VERSION +1 -1
  134. data/service_executables/linux/contrast-service +0 -0
  135. data/service_executables/mac/contrast-service +0 -0
  136. metadata +102 -18
  137. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  138. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  139. data/lib/contrast/extension/protect/kernel.rb +0 -39
  140. data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/protect/rule/base'
5
+ require 'contrast/agent/protect/rule/base_service'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Protect
10
+ module Rule
11
+ module SqlSampleBuilder
12
+ # Generate a sample for the SQL injection detection rule, allowing for reporting to and rendering
13
+ # by TeamServer
14
+ #
15
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
16
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
17
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
18
+ # @candidate_string [String] the value of the input which may be an attack
19
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details
20
+ # to send to the Service to tell the story of the attack
21
+ # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
22
+ module SqliSample
23
+ def build_sample context, input_analysis_result, candidate_string, **kwargs
24
+ sqli_sample = build_base_sample(context, input_analysis_result)
25
+ sqli_sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
26
+ sqli_sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
27
+ sqli_sample.sqli.start_idx = kwargs[:start_idx]
28
+ sqli_sample.sqli.end_idx = kwargs[:end_idx]
29
+ sqli_sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
30
+ sqli_sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
31
+ sqli_sample
32
+ end
33
+ end
34
+
35
+ # Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
36
+ # by TeamServer
37
+ #
38
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
39
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
40
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
41
+ # @candidate_string [String] the value of the input which may be an attack
42
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details
43
+ # to send to the Service to tell the story of the attack
44
+ # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
45
+ module NoSqliSample
46
+ def build_sample context, input_analysis_result, candidate_string, **kwargs
47
+ no_sqli_sample = build_base_sample(context, input_analysis_result)
48
+ no_sqli_sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
49
+ no_sqli_sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
50
+ no_sqli_sample.no_sqli.start_idx = kwargs[:start_idx].to_i
51
+ no_sqli_sample.no_sqli.end_idx = kwargs[:end_idx].to_i
52
+ no_sqli_sample.no_sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
53
+ no_sqli_sample.no_sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
54
+ no_sqli_sample
55
+ end
56
+ end
57
+
58
+ # This Module is how we apply the attack fo NoSQL and SQL Injection rule.
59
+ # It includes methods for building attack with match and database scanners
60
+ module AttackBuilder
61
+ # Set up an attack result and assigns Database scanner for the No-SQL and SQLI injection detection rules
62
+ #
63
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
64
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
65
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
66
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
67
+ # in the case of multiple inputs being found to violate the protection criteria
68
+ # @query_string [string] he value of the input which may be an attack
69
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details to send
70
+ # to the Service to tell the story of the attack
71
+ # @return [Contrast::Api::Dtm::AttackResult] the result from this attack
72
+ def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
73
+ if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
74
+ mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
75
+
76
+ return result
77
+ end
78
+
79
+ attack_string = input_analysis_result.value
80
+ regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
81
+
82
+ return unless query_string.match?(regexp)
83
+
84
+ database = kwargs[:database]
85
+ scanner = select_scanner(database)
86
+ ss = StringScanner.new(query_string)
87
+ length = attack_string.length
88
+ while ss.scan_until(regexp)
89
+ # the pos of StringScanner is at the end of the regexp (input string),
90
+ # we need the beginning
91
+ idx = ss.pos - attack_string.length
92
+ last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
93
+ next unless last_boundary && boundary
94
+
95
+ result ||= build_attack_result(context)
96
+
97
+ record_match(idx, length, boundary, last_boundary, kwargs)
98
+ append_match(context, input_analysis_result, result, query_string, **kwargs)
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ def select_scanner database
105
+ @scanners ||= {
106
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
107
+ Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
108
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
109
+ Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
110
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
111
+ Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new,
112
+ Contrast::Agent::Protect::Policy::AppliesNoSqliRule::DATABASE_NOSQL =>
113
+ Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
114
+ }.cs__freeze
115
+
116
+ @default_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
117
+ @scanners[database.to_s] || @default_scanner
118
+ end
119
+
120
+ def record_match idx, length, boundary, last_boundary, kwargs
121
+ kwargs[:start_idx] = idx
122
+ kwargs[:end_idx] = idx + length
123
+ kwargs[:boundary_overrun_idx] = boundary
124
+ kwargs[:input_boundary_idx] = last_boundary
125
+ end
126
+
127
+ def append_match context, input_analysis_result, result, query_string, **kwargs
128
+ input_analysis_result.attack_count = input_analysis_result.attack_count + 1
129
+ update_successful_attack_response(context, input_analysis_result, result, query_string)
130
+ append_sample(context, input_analysis_result, result, query_string, **kwargs)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/agent/protect/rule/base_service'
5
5
  require 'contrast/agent/protect/policy/applies_sqli_rule'
6
+ require 'contrast/agent/protect/rule/sql_sample_builder'
6
7
 
7
8
  module Contrast
8
9
  module Agent
@@ -10,6 +11,12 @@ module Contrast
10
11
  module Rule
11
12
  # The Ruby implementation of the Protect SQL Injection rule.
12
13
  class Sqli < Contrast::Agent::Protect::Rule::BaseService
14
+ # Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering
15
+ # by TeamServer
16
+ include SqlSampleBuilder::SqliSample
17
+ # Defining build_attack_with_match method
18
+ include SqlSampleBuilder::AttackBuilder
19
+
13
20
  NAME = 'sql-injection'
14
21
  BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
15
22
 
@@ -31,76 +38,6 @@ module Contrast
31
38
 
32
39
  raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
33
40
  end
34
-
35
- def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
36
- attack_string = input_analysis_result.value
37
- regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
38
-
39
- return unless query_string.match?(regexp)
40
-
41
- database = kwargs[:database]
42
- scanner = select_scanner(database)
43
-
44
- ss = StringScanner.new(query_string)
45
- length = attack_string.length
46
- while ss.scan_until(regexp)
47
- # the pos of StringScanner is at the end of the regexp (input string),
48
- # we need the beginning
49
- idx = ss.pos - attack_string.length
50
- last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
51
- next unless last_boundary && boundary
52
-
53
- result ||= build_attack_result(context)
54
- record_match(idx, length, boundary, last_boundary, kwargs)
55
- append_match(context, input_analysis_result, result, query_string, **kwargs)
56
- end
57
-
58
- result
59
- end
60
-
61
- protected
62
-
63
- def build_sample context, input_analysis_result, candidate_string, **kwargs
64
- input = input_analysis_result.value
65
-
66
- sample = build_base_sample(context, input_analysis_result)
67
- sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
68
- sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
69
- sample.sqli.start_idx = sample.sqli.query.index(input).to_i
70
- sample.sqli.end_idx = sample.sqli.start_idx + input.length
71
- sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
72
- sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
73
- sample
74
- end
75
-
76
- private
77
-
78
- def record_match idx, length, boundary, last_boundary, kwargs
79
- kwargs[:start_idx] = idx
80
- kwargs[:end_idx] = idx + length
81
- kwargs[:boundary_overrun_idx] = boundary
82
- kwargs[:input_boundary_idx] = last_boundary
83
- end
84
-
85
- def append_match context, input_analysis_result, result, query_string, **kwargs
86
- input_analysis_result.attack_count = input_analysis_result.attack_count + 1
87
- update_successful_attack_response(context, input_analysis_result, result, query_string)
88
- append_sample(context, input_analysis_result, result, query_string, **kwargs)
89
- end
90
-
91
- def select_scanner database
92
- @sql_scanners ||= {
93
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
94
- Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
95
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
96
- Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
97
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
98
- Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new
99
- }.cs__freeze
100
-
101
- @default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
102
- @sql_scanners[database.to_s] || @default_sql_scanner
103
- end
104
41
  end
105
42
  end
106
43
  end
@@ -9,7 +9,7 @@ module Contrast
9
9
  # Because communication between the Agent/Service and TeamServer can only be initiated by outbound connections
10
10
  # from the Agent/Service, we must provide a mechanism for the TeamServer to direct the Agent to take a specific
11
11
  # action. This action is referred to as a Reaction. This class is how we handle those Reaction messages.
12
- class ReactionProcessor
12
+ module ReactionProcessor
13
13
  extend Contrast::Components::Logger::InstanceMethods
14
14
 
15
15
  # Process the given Reactions from the application settings based on what
@@ -92,10 +92,12 @@ module Contrast
92
92
  defined?(Rack::Multipart::UploadedFile) &&
93
93
  body.is_a?(Rack::Multipart::UploadedFile)
94
94
 
95
- logger.trace("not parsing uploaded file body :: #{ body.original_filename }::#{ body.content_type }")
95
+ logger.trace('not parsing uploaded file body',
96
+ file_name: body.original_filename,
97
+ content_type: body.content_type)
96
98
  @_body = nil
97
99
  else
98
- logger.trace("parsing body from request :: #{ body.cs__class.cs__name }")
100
+ logger.trace('parsing body from request', body_type: body.cs__class.cs__name)
99
101
  @_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
100
102
  end
101
103
 
@@ -183,8 +185,11 @@ module Contrast
183
185
  res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
184
186
  res
185
187
  when Enumerable
186
- res = val.each_with_index.each_with_object({}) do |(v, i), hash|
187
- hash.merge! normalize_params(v, prefix: "#{ prefix }[#{ i }]")
188
+ idx = 0
189
+ res = {}
190
+ while idx < val.length
191
+ res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
192
+ idx += 1
188
193
  end
189
194
  res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
190
195
  res
@@ -4,15 +4,14 @@
4
4
  require 'contrast/utils/timer'
5
5
  require 'contrast/agent/request'
6
6
  require 'contrast/agent/response'
7
- require 'contrast/utils/inventory_util'
7
+ require 'contrast/agent/inventory/database_config'
8
8
  require 'contrast/components/logger'
9
9
  require 'contrast/components/scope'
10
10
 
11
11
  module Contrast
12
12
  module Agent
13
- # This class acts to encapsulate information about the currently executed
14
- # request, making it available to the Agent for the duration of the request
15
- # in a standardized and normalized format which the Agent understands.
13
+ # This class acts to encapsulate information about the currently executed request, making it available to the Agent
14
+ # for the duration of the request in a standardized and normalized format which the Agent understands.
16
15
  #
17
16
  # @attr_reader timer [Contrast::Utils::Timer] when the context was created
18
17
  # @attr_reader logging_hash [Hash] context used to log the request
@@ -61,14 +60,10 @@ module Contrast
61
60
  # generic holder for properties that can be set throughout this request
62
61
  @_properties = {}
63
62
 
64
- @sample = true
65
-
66
63
  if ::Contrast::ASSESS.enabled?
67
- @sample_request, @sample_response = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
64
+ @sample_req, @sample_res = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
68
65
  end
69
66
 
70
- @sample_response &&= ::Contrast::ASSESS.scan_response?
71
-
72
67
  append_route_coverage(Contrast::Agent.framework_manager.get_route_dtm(@request))
73
68
  end
74
69
  end
@@ -78,16 +73,38 @@ module Contrast
78
73
  end
79
74
 
80
75
  def analyze_request?
81
- @sample_request
76
+ analyze_request_assess? || analyze_req_res_protect?
82
77
  end
83
78
 
84
79
  def analyze_response?
85
- @sample_response
80
+ analyze_response_assess? || analyze_req_res_protect?
81
+ end
82
+
83
+ def analyze_req_res_protect?
84
+ ::Contrast::PROTECT.enabled?
86
85
  end
87
86
 
88
- # Convert the discovered route for this request to appropriate forms and
89
- # disseminate it to those locations where it is necessary for our route
90
- # coverage and finding vulnerability discovery features to function.
87
+ def analyze_request_assess?
88
+ return false unless analyze_req_res_assess?
89
+
90
+ @sample_req
91
+ end
92
+
93
+ def analyze_response_assess?
94
+ return false unless analyze_req_res_assess?
95
+
96
+ @sample_res &&= ::Contrast::ASSESS.scan_response?
97
+ end
98
+
99
+ def analyze_req_res_assess?
100
+ ::Contrast::ASSESS.enabled?
101
+ end
102
+
103
+ # Convert the discovered route for this request to appropriate forms and disseminate it to those locations
104
+ # where it is necessary for our route coverage and finding vulnerability discovery features to function.
105
+ #
106
+ # @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the
107
+ # framework
91
108
  def append_route_coverage route
92
109
  return unless route
93
110
 
@@ -108,8 +125,8 @@ module Contrast
108
125
  # Collect the results for the given rule with the given action
109
126
  #
110
127
  # @param rule [String] the id of the rule to which the results apply
111
- # @param response_type [Symbol] the result of the response, matching a
112
- # value of Contrast::Api::Dtm::AttackResult::ResponseType
128
+ # @param response_type [Symbol] the result of the response, matching a value of
129
+ # Contrast::Api::Dtm::AttackResult::ResponseType
113
130
  # @return [Array<Contrast::Api::Dtm::AttackResult>]
114
131
  def results_for rule, response_type = nil
115
132
  if response_type.nil?
@@ -130,8 +147,10 @@ module Contrast
130
147
  handle_protect_state(service_response)
131
148
  ia = service_response.input_analysis
132
149
  if ia
133
- logger.trace("Analysis from Contrast Service: evaluations=#{ ia.results.length }")
134
- logger.trace('Results', input_analysis: ia.inspect)
150
+ if logger.trace?
151
+ logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
152
+ logger.trace('Results', input_analysis: ia.inspect)
153
+ end
135
154
  @speedracer_input_analysis = ia
136
155
  speedracer_input_analysis.request = request
137
156
  else
@@ -145,10 +164,9 @@ module Contrast
145
164
  false
146
165
  end
147
166
 
148
- # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations
149
- # when the protect state indicates a security exception should be thrown. This method
150
- # ensures that the attack reports are generated. Normally these should be generated on
151
- # Speedracer for any attacks detected during prefilter.
167
+ # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state
168
+ # indicates a security exception should be thrown. This method ensures that the attack reports are generated.
169
+ # Normally these should be generated on Speedracer for any attacks detected during prefilter.
152
170
  #
153
171
  # @param agent_settings [Contrast::Api::Settings::AgentSettings]
154
172
  def handle_protect_state agent_settings
@@ -165,12 +183,11 @@ module Contrast
165
183
  raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior'))
166
184
  end
167
185
 
168
- # append anything we've learned to the request seen message
169
- # this is the sum-total of all inventory information that has
170
- # been accumulated since the last request
186
+ # append anything we've learned to the request seen message this is the sum-total of all inventory information
187
+ # that has been accumulated since the last request
171
188
  def extract_after rack_response
172
189
  @response = Contrast::Agent::Response.new(rack_response)
173
- activity.http_response = @response.dtm if @sample_response
190
+ activity.http_response = @response.dtm if @sample_res
174
191
  rescue StandardError => e
175
192
  logger.error('Unable to extract information after request', e)
176
193
  end
@@ -185,14 +202,13 @@ module Contrast
185
202
 
186
203
  def reset_activity
187
204
  @activity = Contrast::Api::Dtm::Activity.new(http_request: request.dtm)
188
- @server_activity = Contrast::Api::Dtm::ServerActivity.new # it doesn't look like this is ever actually used?
205
+ @server_activity = Contrast::Api::Dtm::ServerActivity.new
189
206
  @observed_route = Contrast::Api::Dtm::ObservedRoute.new
190
207
  end
191
208
 
192
209
  private
193
210
 
194
- # Generate attack results directly from any evaluations on the
195
- # agent settings object.
211
+ # Generate attack results directly from any evaluations on the agent settings object.
196
212
  #
197
213
  # @param agent_settings [Contrast::Api::Settings::AgentSettings]
198
214
  def build_attack_results agent_settings
@@ -204,12 +220,14 @@ module Contrast
204
220
  rule = ::Contrast::PROTECT.rule(rule_id)
205
221
  next unless rule
206
222
 
207
- logger.debug('Building attack result from Contrast Service input analysis result', result: ia_result.inspect)
223
+ if logger.debug?
224
+ logger.debug('Building attack result from Contrast Service input analysis result',
225
+ result: ia_result.inspect)
226
+ end
208
227
 
209
228
  attack_result = if rule.mode == :BLOCK
210
- # special case for rules (like reflected xss)
211
- # that used to have an infilter / block
212
- # mode but now are just block at perimeter
229
+ # special case for rules (like reflected xss) that used to have an infilter / block mode
230
+ # but now are just block at perimeter
213
231
  rule.build_attack_with_match(self, ia_result, attack_results_by_rule[rule_id],
214
232
  ia_result.value)
215
233
  else
@@ -6,19 +6,23 @@ require 'contrast/components/scope'
6
6
 
7
7
  module Contrast
8
8
  module Agent
9
- # This class is instantiated when we receive a request and the agent is enabled to process
10
- # that request. It holds the ruleset that we perform filtering operations on (currently
11
- # prefilter and postfilter).
9
+ # This class is instantiated when we receive a request and the agent is enabled to process that request. It holds
10
+ # the ruleset that we perform filtering operations on (currently prefilter and postfilter).
12
11
  class RequestHandler
13
12
  include Contrast::Components::Logger::InstanceMethods
14
13
 
15
14
  attr_reader :ruleset, :context
16
15
 
16
+ # @param context [Contrast::Agent::RequestContext] the context of the request for which this handler applies
17
17
  def initialize context
18
18
  @context = context
19
19
  @ruleset = ::Contrast::AGENT.ruleset
20
20
  end
21
21
 
22
+ # TODO: RUBY-1353
23
+ # TODO: RUBY-1355
24
+ # TODO: RUBY-1357
25
+ # TODO: RUBY-1358
22
26
  def send_activity_messages
23
27
  Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.generate_library_usage(context.activity)
24
28
  [context.server_activity, context.activity, context.observed_route].each do |message|
@@ -16,8 +16,7 @@ module Contrast
16
16
  # terminate requests on attack detection if set to block at perimeter
17
17
  def prefilter
18
18
  context = Contrast::Agent::REQUEST_TRACKER.current
19
- # TODO: RUBY-801 We shouldn't be responsible for knowing what modes are enabled
20
- return unless context&.analyze_request? || ::Contrast::PROTECT.enabled?
19
+ return unless context&.analyze_request?
21
20
 
22
21
  logger.trace_with_time('Running prefilter...') do
23
22
  map { |rule| rule.prefilter(context) }
@@ -33,8 +32,7 @@ module Contrast
33
32
  # has been created. The main actions here are analyzing the response for unsafe state or actions.
34
33
  def postfilter
35
34
  context = Contrast::Agent::REQUEST_TRACKER.current
36
- # TODO: RUBY-801 We shouldn't be responsible for knowing what modes are enabled
37
- return unless context&.analyze_response? || ::Contrast::PROTECT.enabled?
35
+ return unless context&.analyze_response?
38
36
 
39
37
  logger.trace_with_time('Running postfilter...') do
40
38
  map { |rule| rule.postfilter(context) }
@@ -104,39 +104,51 @@ module Contrast
104
104
  exit_split_scope!
105
105
  end
106
106
 
107
- # Dynamic versions of the above.
108
- # These are equivalent, but they're slower and riskier.
109
- # Prefer the static methods if you know what scope you need at the call site.
107
+ # Static methods to be used, the cases are defined by the usage from the above methods
108
+ # if more methods are added - please extend the case statements as they are no longed dynamic
110
109
  def in_scope? name
111
- cs__class.ensure_valid_scope! name
112
- call = with_contrast_scope { :"in_#{ name }_scope?" }
113
- send(call)
110
+ case name
111
+ when :contrast
112
+ in_contrast_scope?
113
+ when :deserialization
114
+ in_deserialization_scope?
115
+ when :split
116
+ in_split_scope?
117
+ else
118
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
119
+ end
114
120
  end
115
121
 
116
122
  def enter_scope! name
117
- cs__class.ensure_valid_scope! name
118
- call = with_contrast_scope { :"enter_#{ name }_scope!" }
119
- send(call)
123
+ case name
124
+ when :contrast
125
+ enter_contrast_scope!
126
+ when :deserialization
127
+ enter_deserialization_scope!
128
+ when :split
129
+ enter_split_scope!
130
+ else
131
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
132
+ end
120
133
  end
121
134
 
122
135
  def exit_scope! name
123
- cs__class.ensure_valid_scope! name
124
- call = with_contrast_scope { :"exit_#{ name }_scope!" }
125
- send(call)
136
+ case name
137
+ when :contrast
138
+ exit_contrast_scope!
139
+ when :deserialization
140
+ exit_deserialization_scope!
141
+ when :split
142
+ exit_split_scope!
143
+ else
144
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
145
+ end
126
146
  end
127
147
 
128
148
  class << self
129
149
  def valid_scope? scope_sym
130
150
  Contrast::Agent::Scope::SCOPE_LIST.include? scope_sym
131
151
  end
132
-
133
- def ensure_valid_scope! scope_sym
134
- unless valid_scope? scope_sym # rubocop:disable Style/GuardClause
135
- with_contrast_scope do
136
- raise NoMethodError, "Scope '#{ scope_sym.inspect }' is not registered as a scope."
137
- end
138
- end
139
- end
140
152
  end
141
153
  end
142
154
  end
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/metrics_hash'
5
+ require 'contrast/agent/metric_telemetry_event'
6
+ require 'contrast/agent/version'
7
+ require 'contrast/utils/os'
8
+
9
+ module Contrast
10
+ module Agent
11
+ # This class will hold the Startup Metrics Telemetry Event
12
+ # The class will include initialization of the agent version, language version
13
+ # os type, arch and version
14
+ # application framework and version and server framework
15
+ # It will be initialized and send in Middleware#agent_startup_routine
16
+ class StartupMetricsTelemetryEvent < Contrast::Agent::MetricTelemetryEvent
17
+ include Contrast::Utils::OS
18
+
19
+ APP_AND_SERVER_DATA = ::Contrast::APP_CONTEXT.app_and_server_information.cs__freeze
20
+ SAAS_DEFAULT = { addr: 'app.contrastsecuirty.com', type: 'SAAS_DEFAULT' }.cs__freeze
21
+ SAAS_CE = { addr: 'ce.contrastsecurity.com', type: 'SAAS_CE' }.cs__freeze
22
+ SAAS_CUSTOM = { addr: 'contrastsecurite.com', type: 'SAAS_CUSTOM' }.cs__freeze
23
+ SAAS_POV = { addr: 'eval.contrastsecuirty.com', type: 'SAAS_POV' }.cs__freeze
24
+ EOP = 'EOP'
25
+
26
+ def initialize
27
+ super
28
+ add_tags
29
+ end
30
+
31
+ def path
32
+ '/startup'
33
+ end
34
+
35
+ def add_tags
36
+ @tags['teamserver'] = teamserver_type
37
+ @tags['agent_version'] = VERSION
38
+ @tags['ruby_version'] = RUBY_VERSION
39
+ @tags['os_type'] = sys_info['os_type'] == 'Darwin' ? 'MacOS' : 'Linux'
40
+ @tags['os_arch'] = sys_info['os_arch']
41
+ @tags['os_version'] = sys_info['os_version']
42
+ @tags['app_framework_and_version'] = APP_AND_SERVER_DATA[:application_info].to_s
43
+ @tags['server_framework_and_version'] = APP_AND_SERVER_DATA[:server_info].to_s
44
+ end
45
+
46
+ def sys_info
47
+ @sys_info ||= get_system_information if @sys_info.nil?
48
+ @sys_info
49
+ end
50
+
51
+ private
52
+
53
+ # Here we extract the Teamserver url type
54
+ #
55
+ # @return[String] type, it could be SAAS_DEFAULT, SAAS_POV, SAAS_CE, SAAS_CUSTOM, or EOP
56
+ def teamserver_type
57
+ @_teamserver_type ||= if Contrast::API.api_url.include?(SAAS_DEFAULT[:addr])
58
+ SAAS_DEFAULT[:type]
59
+ elsif Contrast::API.api_url.include?(SAAS_POV[:addr])
60
+ SAAS_POV[:type]
61
+ elsif Contrast::API.api_url.include?(SAAS_CE[:addr])
62
+ SAAS_CE[:type]
63
+ elsif Contrast::API.api_url.end_with? SAAS_CUSTOM[:addr]
64
+ SAAS_CUSTOM[:type]
65
+ else
66
+ EOP
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -14,8 +14,8 @@ module Contrast
14
14
  include Contrast::Components::Scope::InstanceMethods
15
15
 
16
16
  class << self
17
- # After the first request is complete, we do a one-time manual catchup to review and
18
- # report the already-loaded gems.
17
+ # After the first request is complete, we do a one-time manual catchup to review and report the already-loaded
18
+ # gems.
19
19
  def catchup
20
20
  @_catchup ||= begin
21
21
  threaded_analysis!
@@ -23,12 +23,14 @@ module Contrast
23
23
  end
24
24
  end
25
25
 
26
+ # TODO: RUBY-1354
27
+ # TODO: RUBY-1356
26
28
  def send_inventory_message
27
29
  return unless ::Contrast::INVENTORY.enabled?
28
30
 
29
31
  app_update_msg = Contrast::Api::Dtm::ApplicationUpdate.build
30
32
 
31
- Contrast::Utils::InventoryUtil.append_db_config(app_update_msg)
33
+ Contrast::Agent::Inventory::DatabaseConfig.append_db_config(app_update_msg)
32
34
  Contrast::Agent.messaging_queue.send_event_eventually(app_update_msg)
33
35
  end
34
36