contrast-agent 4.13.1 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
- data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
- data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +51 -57
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
- data/lib/contrast/agent/middleware.rb +5 -75
- data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
- data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
- data/lib/contrast/agent/reporting/report.rb +21 -0
- data/lib/contrast/agent/reporting/reporter.rb +142 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
- data/lib/contrast/agent/request.rb +2 -81
- data/lib/contrast/agent/request_context.rb +4 -128
- data/lib/contrast/agent/request_context_extend.rb +138 -0
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/telemetry.rb +15 -7
- data/lib/contrast/agent/telemetry_event.rb +8 -9
- data/lib/contrast/agent/thread_watcher.rb +31 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +15 -0
- data/lib/contrast/api/communication/connection_status.rb +10 -7
- data/lib/contrast/api/communication/messaging_queue.rb +37 -3
- data/lib/contrast/api/communication/response_processor.rb +15 -8
- data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
- data/lib/contrast/api/communication/socket.rb +6 -8
- data/lib/contrast/api/communication/socket_client.rb +29 -12
- data/lib/contrast/api/communication/speedracer.rb +37 -1
- data/lib/contrast/api/communication/tcp_socket.rb +4 -3
- data/lib/contrast/api/communication/unix_socket.rb +1 -0
- data/lib/contrast/api/decorators/finding.rb +45 -0
- data/lib/contrast/components/api.rb +56 -0
- data/lib/contrast/components/app_context.rb +10 -65
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +8 -8
- data/lib/contrast/components/contrast_service.rb +5 -0
- data/lib/contrast/components/sampling.rb +2 -2
- data/lib/contrast/config/agent_configuration.rb +1 -1
- data/lib/contrast/config/api_configuration.rb +9 -4
- data/lib/contrast/config/api_proxy_configuration.rb +14 -0
- data/lib/contrast/config/application_configuration.rb +2 -3
- data/lib/contrast/config/assess_configuration.rb +3 -3
- data/lib/contrast/config/base_configuration.rb +17 -28
- data/lib/contrast/config/certification_configuration.rb +15 -0
- data/lib/contrast/config/env_variables.rb +2 -9
- data/lib/contrast/config/heap_dump_configuration.rb +6 -6
- data/lib/contrast/config/inventory_configuration.rb +1 -5
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/request_audit_configuration.rb +18 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +1 -2
- data/lib/contrast/config.rb +0 -1
- data/lib/contrast/configuration.rb +1 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/framework/manager.rb +8 -32
- data/lib/contrast/framework/manager_extend.rb +50 -0
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
- data/lib/contrast/utils/class_util.rb +18 -14
- data/lib/contrast/utils/findings.rb +62 -0
- data/lib/contrast/utils/hash_digest.rb +10 -73
- data/lib/contrast/utils/hash_digest_extend.rb +86 -0
- data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
- data/lib/contrast/utils/heap_dump_util.rb +2 -65
- data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +108 -0
- data/lib/contrast/utils/middleware_utils.rb +87 -0
- data/lib/contrast/utils/net_http_base.rb +158 -0
- data/lib/contrast/utils/object_share.rb +1 -0
- data/lib/contrast/utils/request_utils.rb +88 -0
- data/lib/contrast/utils/response_utils.rb +97 -0
- data/lib/contrast/utils/substitution_utils.rb +167 -0
- data/lib/contrast/utils/tag_util.rb +9 -9
- data/lib/contrast/utils/telemetry.rb +4 -2
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +17 -24
- data/ruby-agent.gemspec +5 -5
- metadata +48 -23
- data/lib/contrast/config/default_value.rb +0 -17
- data/lib/contrast/utils/requests_client.rb +0 -150
|
@@ -0,0 +1,56 @@
|
|
|
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/components/logger'
|
|
5
|
+
require 'contrast/agent/reporting/reporting_events/reporting_event'
|
|
6
|
+
|
|
7
|
+
module Contrast
|
|
8
|
+
module Agent
|
|
9
|
+
module Reporting
|
|
10
|
+
# This is the new PreflightMessage class which will include all the needed information
|
|
11
|
+
# for the new reporting system to report a single message, part of the main Preflight
|
|
12
|
+
class PreflightMessage < Contrast::Agent::Reporting::ReportingEvent
|
|
13
|
+
attr_accessor :data, :routes, :hash_code
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@event_type = :preflight_message
|
|
18
|
+
@app_language = Contrast::Utils::ObjectShare::RUBY
|
|
19
|
+
@app_name = ::Contrast::APP_CONTEXT.app_name
|
|
20
|
+
@app_version = ::Contrast::APP_CONTEXT.app_version
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_controlled_hash **_args
|
|
24
|
+
validate
|
|
25
|
+
super.merge!({
|
|
26
|
+
app_language: @app_language,
|
|
27
|
+
app_name: @app_name,
|
|
28
|
+
app_version: @app_version,
|
|
29
|
+
data: '',
|
|
30
|
+
key: 0,
|
|
31
|
+
routes: @routes,
|
|
32
|
+
session_id: @agent_session_id_value
|
|
33
|
+
})
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate
|
|
37
|
+
raise(ArgumentError, "#{ cs__class } did not have a proper data. Unable to continue.") unless data
|
|
38
|
+
unless @app_name
|
|
39
|
+
raise(ArgumentError, "#{ cs__class } did not have a proper application name. Unable to continue.")
|
|
40
|
+
end
|
|
41
|
+
unless @app_language
|
|
42
|
+
raise(ArgumentError, "#{ cs__class } did not have a proper application language. Unable to continue.")
|
|
43
|
+
end
|
|
44
|
+
unless @app_version
|
|
45
|
+
raise(ArgumentError, "#{ cs__class } did not have a proper application version. Unable to continue.")
|
|
46
|
+
end
|
|
47
|
+
unless @agent_session_id_value
|
|
48
|
+
raise(ArgumentError, "#{ cs__class } did not have a proper session id. Unable to continue.")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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/components/logger'
|
|
5
|
+
require 'contrast/utils/assess/trigger_method_utils'
|
|
6
|
+
|
|
7
|
+
module Contrast
|
|
8
|
+
module Agent
|
|
9
|
+
module Reporting
|
|
10
|
+
# This is the new ReportingEvent class which will include all the needed and mutual information
|
|
11
|
+
# for the new reporting system
|
|
12
|
+
# @abstract
|
|
13
|
+
class ReportingEvent
|
|
14
|
+
attr_reader :routes
|
|
15
|
+
attr_accessor :event_type
|
|
16
|
+
|
|
17
|
+
CODE = :TRACE
|
|
18
|
+
|
|
19
|
+
def initialize(*) # rubocop:disable Style/MethodDefParentheses
|
|
20
|
+
@event_type = Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
21
|
+
@agent_session_id_value = 0
|
|
22
|
+
@routes = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_controlled_hash **_args
|
|
26
|
+
{ code: CODE, routes: routes }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate
|
|
30
|
+
raise(ArgumentError, "#{ self } did not have a proper routes. Unable to continue.") if routes.empty?
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
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/components/logger'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Contrast
|
|
9
|
+
module Agent
|
|
10
|
+
module Reporting
|
|
11
|
+
# This class will facilitate the Audit functionality and it will be
|
|
12
|
+
# controlled from the configuration classes
|
|
13
|
+
class Audit
|
|
14
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
15
|
+
|
|
16
|
+
attr_reader :path_for_requests, :path_for_responses
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
generate_paths if enabled? && Contrast::CONTRAST_SERVICE.use_agent_communication?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# This method will be handling the auditing of the requests and responses we send to SpeedRacer. If the audit
|
|
23
|
+
# feature is enabled, we'll log to file the request and/or response protobuf objects.
|
|
24
|
+
#
|
|
25
|
+
# @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of
|
|
26
|
+
# Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity
|
|
27
|
+
# @param response_data [Contrast::Api::Settings::AgentSettings,nil]
|
|
28
|
+
def audit_event event, response_data = nil
|
|
29
|
+
return unless ::Contrast::API.request_audit_requests || ::Contrast::API.request_audit_responses
|
|
30
|
+
|
|
31
|
+
type = event.cs__respond_to?(:file_name) ? event.file_name : event.cs__class.cs__name.to_s.downcase
|
|
32
|
+
if ::Contrast::API.request_audit_requests
|
|
33
|
+
data = event.to_s
|
|
34
|
+
log_data :request, type, data if data
|
|
35
|
+
end
|
|
36
|
+
return unless ::Contrast::API.request_audit_responses
|
|
37
|
+
|
|
38
|
+
data = response_data.to_s || event.http_response.try(:body) || 'There is no available response'
|
|
39
|
+
log_data :response, type, data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# This method will proceed with passing the data with to the writing method
|
|
45
|
+
# @param type [Symbol] This is the type of the file /:request, :response/
|
|
46
|
+
# @param data_type[String] DTM type String representation
|
|
47
|
+
# @param data[String] String representation if the logged data
|
|
48
|
+
def log_data type, data_type, data = nil
|
|
49
|
+
return unless enabled?
|
|
50
|
+
return unless Contrast::CONTRAST_SERVICE.use_agent_communication?
|
|
51
|
+
|
|
52
|
+
write_to_file type, data_type, data
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# This method will be actually writing to the file
|
|
56
|
+
# @param type [Symbol] This is the type of the file /:request, :response/
|
|
57
|
+
# @param data_type [String] Data type /Activity/Finding../
|
|
58
|
+
# @param data [any] The data to be written to the file
|
|
59
|
+
def write_to_file type, data_type, data = nil
|
|
60
|
+
time = Time.now.to_i
|
|
61
|
+
destination = type == :request ? path_for_requests : path_for_responses
|
|
62
|
+
# If the feature is disabled or we have yet to create the directory structure, then we could have a nil
|
|
63
|
+
# destination. In that case, take no action
|
|
64
|
+
return unless destination
|
|
65
|
+
|
|
66
|
+
filename = "#{ time }-#{ data_type.gsub('::', '_') }-teamserver.json"
|
|
67
|
+
filepath = File.join(destination, filename)
|
|
68
|
+
# Here is use append mode, because of a slightly possibility of overwriting an existing file
|
|
69
|
+
File.open(filepath, 'a') do |f|
|
|
70
|
+
f.write({ data_type: Contrast::Utils::StringUtils.force_utf8(data) }.to_json)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Here we will generate the directories for the requests and responses
|
|
75
|
+
def generate_paths
|
|
76
|
+
message_directories = File.expand_path(path_to_audits)
|
|
77
|
+
FileUtils.mkdir_p(message_directories) unless Dir.exist?(message_directories)
|
|
78
|
+
|
|
79
|
+
requests_destination = File.expand_path(File.join(message_directories, '/requests'))
|
|
80
|
+
responses_destination = File.expand_path(File.join(message_directories, '/responses'))
|
|
81
|
+
|
|
82
|
+
Dir.mkdir(requests_destination) if enabled_for_requests? && !Dir.exist?(requests_destination)
|
|
83
|
+
Dir.mkdir(responses_destination) if enabled_for_responses? && !Dir.exist?(responses_destination)
|
|
84
|
+
|
|
85
|
+
@path_for_requests ||= requests_destination if enabled_for_requests?
|
|
86
|
+
@path_for_responses ||= responses_destination if enabled_for_responses?
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
logger.warn('Generating the paths failed with: ', e: e)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Retrieves the configuration value if the request audit is enabled
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def enabled?
|
|
94
|
+
::Contrast::API.request_audit_enable
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The boolean values for the requests and the responses should be taken under
|
|
98
|
+
# consideration only if it's in combination with enabled
|
|
99
|
+
# So in order for us to actually audit the requests, we need:
|
|
100
|
+
# - enabled? -> ture and enabled_for_requests? -> true
|
|
101
|
+
# The same is for the responses
|
|
102
|
+
#
|
|
103
|
+
#
|
|
104
|
+
# Retrieve the configuration value if the audit for requests is enabled
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def enabled_for_requests?
|
|
107
|
+
::Contrast::API.request_audit_requests
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Retrieve the configuration value if the audit for responses is enabled
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def enabled_for_responses?
|
|
113
|
+
::Contrast::API.request_audit_requests
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Retrieve the configuration value for the path of the audits
|
|
117
|
+
# The value will be read from the configuration yml
|
|
118
|
+
# but if it isn't defined any - here will be returned the default path from
|
|
119
|
+
# Contrast::Config::RequestAuditConfiguration::DEFAULT_PATH
|
|
120
|
+
# @return [String]
|
|
121
|
+
def path_to_audits
|
|
122
|
+
::Contrast::API.request_audit_path
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
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 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'contrast/components/logger'
|
|
7
|
+
require 'contrast/utils/net_http_base'
|
|
8
|
+
require 'contrast/agent/reporting/reporting_events/finding'
|
|
9
|
+
require 'contrast/agent/reporting/reporting_events/preflight_message'
|
|
10
|
+
require 'contrast/agent/reporting/reporting_events/reporting_event'
|
|
11
|
+
|
|
12
|
+
module Contrast
|
|
13
|
+
module Utils
|
|
14
|
+
# This module creates a Net::HTTP client and initiates a connection to the provided result
|
|
15
|
+
class ReporterClient < NetHttpBase
|
|
16
|
+
include ObjectShare
|
|
17
|
+
SERVICE_NAME = 'Reporter'
|
|
18
|
+
ENDPOINTS = {
|
|
19
|
+
application_activity: '/api/ng/activity/application',
|
|
20
|
+
application_create: '/api/ng/applications/create',
|
|
21
|
+
agent_startup: '/api/ng/servers/',
|
|
22
|
+
preflight: '/api/ng/preflight/',
|
|
23
|
+
report_vulnerability: '/api/ng/traces',
|
|
24
|
+
server_activity: '/api/ng/servers/',
|
|
25
|
+
server_update: '/api/ng/update/application'
|
|
26
|
+
}.cs__freeze
|
|
27
|
+
# EVENT_TYPES = {
|
|
28
|
+
# agent_startup: Contrast::Api::Dtm::AgentStartup,
|
|
29
|
+
# report_vulnerability: Contrast::Agent::Reporting::Finding,
|
|
30
|
+
# preflight: Contrast::Agent::Reporting::Preflight
|
|
31
|
+
# }.cs__freeze
|
|
32
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
33
|
+
# This method initializes the Net::HTTP client we'll need. it will validate
|
|
34
|
+
# the connection and make the first request. If connection is valid and response
|
|
35
|
+
# is available then the open connection is returned.
|
|
36
|
+
#
|
|
37
|
+
# @return [Net::HTTP, nil] Return open connection or nil
|
|
38
|
+
def initialize_connection
|
|
39
|
+
# for this client we would use proxy and custom certificate file if available
|
|
40
|
+
super(SERVICE_NAME, Contrast::API.api_url, true, true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param headers [Hash] all the headers needed for communication with TS
|
|
44
|
+
def initialize headers
|
|
45
|
+
@headers = headers
|
|
46
|
+
super()
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Send Agent Startup event
|
|
50
|
+
#
|
|
51
|
+
# @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of
|
|
52
|
+
# Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity
|
|
53
|
+
# @param connection [Net::HTTP] open connection
|
|
54
|
+
# @return response [Net::HTTP::Response] response from TS
|
|
55
|
+
def send_agent_startup event, connection
|
|
56
|
+
logger.debug('Preparing to send startup messages')
|
|
57
|
+
request = build_request ENDPOINTS[:agent_startup], event
|
|
58
|
+
response = connection.request(request)
|
|
59
|
+
logger.debug('Successfully sent startup messages to service.') if response
|
|
60
|
+
response
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check event type and send it to appropriate TS endpoint
|
|
64
|
+
#
|
|
65
|
+
# @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent] One of the DTMs valid for the event
|
|
66
|
+
# field of Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity
|
|
67
|
+
# @param connection [Net::HTTP] open connection
|
|
68
|
+
# @param send_immediately [Boolean] flag for the logger
|
|
69
|
+
# @return response [Net::HTTP::Response, nil] response from TS if no response
|
|
70
|
+
def send_event event, connection, send_immediately = false
|
|
71
|
+
event_type = symbolized_event_type event.event_type
|
|
72
|
+
@response = send_events event, event_type, connection
|
|
73
|
+
log_send_event event if send_immediately
|
|
74
|
+
@response
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# This method will build headers of the request required for TS communication
|
|
80
|
+
#
|
|
81
|
+
# @param request [Net::HTTP::Post]
|
|
82
|
+
def build_headers request
|
|
83
|
+
@app_version = @headers[:app_version]
|
|
84
|
+
request['API-Key'] = @headers[:api_key]
|
|
85
|
+
request['Application-Language'] = @headers[:app_language]
|
|
86
|
+
request['Application-Name'] = @headers[:app_name]
|
|
87
|
+
request['Application-Path'] = @headers[:app_path]
|
|
88
|
+
request['Application-Version'] = @app_version if @app_version
|
|
89
|
+
request['Authorization'] = @headers[:authorization]
|
|
90
|
+
request['Content-Type'] = @headers[:content_type]
|
|
91
|
+
request['Server-Name'] = @headers[:server_name]
|
|
92
|
+
request['Server-Path'] = @headers[:server_path]
|
|
93
|
+
request['Server-Type'] = @headers[:server_type]
|
|
94
|
+
request['X-Contrast-Agent'] = @headers[:agent_version]
|
|
95
|
+
request['X-Contrast-Header-Encoding'] = @headers[:encoding]
|
|
96
|
+
request
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# build the request headers and assign endpoint
|
|
100
|
+
#
|
|
101
|
+
# @param endpoint [String] endpoint to report to
|
|
102
|
+
# @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent]
|
|
103
|
+
# One of the DTMs valid for the event field of Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity
|
|
104
|
+
# @return [Net::HTTP::Post]
|
|
105
|
+
def build_request endpoint, event
|
|
106
|
+
@request = build_headers Net::HTTP::Post.new(endpoint)
|
|
107
|
+
@request.body = if event.cs__is_a?(Contrast::Agent::Reporting::ReportingEvent)
|
|
108
|
+
event.to_controlled_hash.to_json
|
|
109
|
+
else
|
|
110
|
+
event.to_json
|
|
111
|
+
end
|
|
112
|
+
@request
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# log the event sent immediately
|
|
116
|
+
#
|
|
117
|
+
# @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent] One of the DTMs valid for the
|
|
118
|
+
# event field of Contrast::Api::Dtm::Message|Contrast::Api::Dtm::Activity
|
|
119
|
+
def log_send_event event
|
|
120
|
+
logger.debug('Immediately sending event.', event_id: event.__id__, event_type: event.cs__class.cs__name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Sent different events to different endpoints
|
|
124
|
+
#
|
|
125
|
+
# @param event [Contrast::Api::Dtm,Contrast::Agent::Reporting::ReportingEvent] Main reporting event, all events
|
|
126
|
+
# inherit it
|
|
127
|
+
# @param event_type [Symbol] We'll use this symbol to match the urls
|
|
128
|
+
# @param connection [Net::HTTP] open connection
|
|
129
|
+
def send_events event, event_type, connection
|
|
130
|
+
logger.debug("Preparing to send #{ event_type.to_s.capitalize } messages")
|
|
131
|
+
request = build_request ENDPOINTS[event_type], event
|
|
132
|
+
response = connection.request(request)
|
|
133
|
+
logger.debug("Successfully sent #{ event_type.to_s.capitalize } messages to service.") if response
|
|
134
|
+
response
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Eventually here we'll handle more response types and etc
|
|
138
|
+
|
|
139
|
+
# @param event [Contrast::Agent::Reporting::Preflight] the preflight we handle here
|
|
140
|
+
# @param response [Net::HTTP::Response,nil] The response we handle and read from
|
|
141
|
+
# @param connection [Net::HTTP] open connection
|
|
142
|
+
def handle_response event, response, connection
|
|
143
|
+
return unless event || response || connection
|
|
144
|
+
|
|
145
|
+
preflight_message = event.messages[0]
|
|
146
|
+
event_type = symbolized_event_type preflight_message.event_type
|
|
147
|
+
return unless event_type == :preflight_message
|
|
148
|
+
|
|
149
|
+
# for handling multiple findings
|
|
150
|
+
# we'll only extract the indexes without *
|
|
151
|
+
# findings_to_return = response.body.split(',').delete_if { |el| el.include?('*') }
|
|
152
|
+
# after that we'll do some magic and return them the same way we do for corresponding_finding
|
|
153
|
+
corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message.hash_code)
|
|
154
|
+
return unless corresponding_finding
|
|
155
|
+
|
|
156
|
+
send_event corresponding_finding, connection, true
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def symbolized_event_type event_type
|
|
161
|
+
return unless event_type
|
|
162
|
+
return event_type unless event_type.cs__is_a?(String)
|
|
163
|
+
|
|
164
|
+
event_type.downcase.to_sym
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
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/components/logger'
|
|
5
|
+
|
|
6
|
+
module Contrast
|
|
7
|
+
module Agent
|
|
8
|
+
module Reporting
|
|
9
|
+
# This module will be used to store everything that would be sent
|
|
10
|
+
# and depends on the response we receive
|
|
11
|
+
module ReportingStorage
|
|
12
|
+
class << self
|
|
13
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
14
|
+
|
|
15
|
+
# So the collection will be used to store those events
|
|
16
|
+
def collection
|
|
17
|
+
@_collection ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param key[String] the key for the pair
|
|
21
|
+
# @param value[Object] the value we need to store
|
|
22
|
+
# @return [Object] the value we saved
|
|
23
|
+
def []= key, value
|
|
24
|
+
return unless key
|
|
25
|
+
return unless value
|
|
26
|
+
|
|
27
|
+
logger.debug('Saving new value', key: key)
|
|
28
|
+
|
|
29
|
+
key = key.to_s.downcase.strip
|
|
30
|
+
collection[key] = value
|
|
31
|
+
|
|
32
|
+
value # rubocop:disable Lint/Void
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param key[String] the key for the pair
|
|
36
|
+
def [] key
|
|
37
|
+
return unless key
|
|
38
|
+
|
|
39
|
+
collection[key]
|
|
40
|
+
end
|
|
41
|
+
alias_method :get, :[]
|
|
42
|
+
alias_method :set, :[]=
|
|
43
|
+
|
|
44
|
+
def delete key
|
|
45
|
+
return unless key
|
|
46
|
+
|
|
47
|
+
logger.debug('Starting deleting value for', key: key)
|
|
48
|
+
|
|
49
|
+
deleted_value = collection.delete(key)
|
|
50
|
+
logger.debug('Key wasn\'t found') unless deleted_value
|
|
51
|
+
|
|
52
|
+
deleted_value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param rule_id [String] the rule_id
|
|
56
|
+
# @return [Hash, nil] return array with key and value of the pair
|
|
57
|
+
def find_by_rule_id rule_id
|
|
58
|
+
return unless rule_id
|
|
59
|
+
|
|
60
|
+
collection.find { |_, v| v.rule_id == rule_id }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -9,6 +9,7 @@ require 'contrast/utils/string_utils'
|
|
|
9
9
|
require 'contrast/utils/hash_digest'
|
|
10
10
|
require 'contrast/components/logger'
|
|
11
11
|
require 'contrast/components/scope'
|
|
12
|
+
require 'contrast/utils/request_utils'
|
|
12
13
|
|
|
13
14
|
module Contrast
|
|
14
15
|
module Agent
|
|
@@ -17,6 +18,7 @@ module Contrast
|
|
|
17
18
|
# data in a format that the Agent expects, caching those transformations in
|
|
18
19
|
# order to avoid repeatedly creating Strings & thrashing GC.
|
|
19
20
|
class Request
|
|
21
|
+
include Contrast::Utils::RequestUtils
|
|
20
22
|
include Contrast::Components::Logger::InstanceMethods
|
|
21
23
|
include Contrast::Components::Scope::InstanceMethods
|
|
22
24
|
|
|
@@ -152,87 +154,6 @@ module Contrast
|
|
|
152
154
|
|
|
153
155
|
accepts.start_with?(*MEDIA_TYPE_MARKERS)
|
|
154
156
|
end
|
|
155
|
-
|
|
156
|
-
private
|
|
157
|
-
|
|
158
|
-
# Return a flattened hash of params with realized paths for keys, in
|
|
159
|
-
# addition to a separate, valueless, entry for each nest key.
|
|
160
|
-
# See RUBY-621 for more details.
|
|
161
|
-
# { key : { nested_key : ['x','y','z' ] } }
|
|
162
|
-
# becomes
|
|
163
|
-
# {
|
|
164
|
-
# key[nested_key][0] : 'x'
|
|
165
|
-
# key[nested_key][1] : 'y'
|
|
166
|
-
# key[nested_key][2] : 'z'
|
|
167
|
-
# key : ''
|
|
168
|
-
# nested_key : ''
|
|
169
|
-
# }
|
|
170
|
-
def normalize_params val, prefix: nil
|
|
171
|
-
# In non-recursive invocations, val should always be a Hash
|
|
172
|
-
# (rather than breaking this out into two methods)
|
|
173
|
-
case val
|
|
174
|
-
when Tempfile
|
|
175
|
-
# Skip if it's the auto-generated value from rails when it handles
|
|
176
|
-
# file uploads. The file name will still be sent to SR for analysis.
|
|
177
|
-
{}
|
|
178
|
-
when Hash
|
|
179
|
-
res = val.each_with_object({}) do |(k, v), hash|
|
|
180
|
-
k = Contrast::Utils::StringUtils.force_utf8(k)
|
|
181
|
-
nested_prefix = prefix.nil? ? k : "#{ prefix }[#{ k }]"
|
|
182
|
-
hash[k] = Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
183
|
-
hash.merge! normalize_params(v, prefix: nested_prefix)
|
|
184
|
-
end
|
|
185
|
-
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
|
186
|
-
res
|
|
187
|
-
when Enumerable
|
|
188
|
-
idx = 0
|
|
189
|
-
res = {}
|
|
190
|
-
while idx < val.length
|
|
191
|
-
res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
|
|
192
|
-
idx += 1
|
|
193
|
-
end
|
|
194
|
-
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
|
195
|
-
res
|
|
196
|
-
else
|
|
197
|
-
{ prefix => Contrast::Utils::StringUtils.force_utf8(val) }
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def read_body body
|
|
202
|
-
return body if body.is_a?(String)
|
|
203
|
-
|
|
204
|
-
begin
|
|
205
|
-
can_rewind = Contrast::Utils::DuckUtils.quacks_to?(body, :rewind)
|
|
206
|
-
# if we are after a middleware that failed to rewind
|
|
207
|
-
body.rewind if can_rewind
|
|
208
|
-
body.read
|
|
209
|
-
rescue StandardError => e
|
|
210
|
-
logger.error('Error in attempt to read body', message: e.message)
|
|
211
|
-
logger.trace('With Stack', e)
|
|
212
|
-
body.to_s
|
|
213
|
-
ensure
|
|
214
|
-
# be a good citizen and rewind
|
|
215
|
-
body.rewind if can_rewind
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def traverse_parsed_multipart multipart_data, current_names
|
|
220
|
-
return current_names unless multipart_data
|
|
221
|
-
|
|
222
|
-
multipart_data.each_value do |data_value|
|
|
223
|
-
next unless data_value.is_a?(Hash)
|
|
224
|
-
|
|
225
|
-
tempfile = data_value[:tempfile]
|
|
226
|
-
if tempfile.nil?
|
|
227
|
-
traverse_parsed_multipart(data_value, current_names)
|
|
228
|
-
else
|
|
229
|
-
name = data_value[:name].to_s
|
|
230
|
-
file_name = data_value[:filename].to_s
|
|
231
|
-
current_names[name] = file_name
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
current_names
|
|
235
|
-
end
|
|
236
157
|
end
|
|
237
158
|
end
|
|
238
159
|
end
|