contrast-agent 4.13.1 → 4.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|