newrelic_security 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pr_ci.yml +4 -4
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/rubocop.yml +1 -1
- data/CHANGELOG.md +90 -1
- data/Gemfile_test +3 -0
- data/README.md +1 -0
- data/THIRD_PARTY_NOTICES.md +8 -0
- data/lib/newrelic_security/agent/agent.rb +22 -4
- data/lib/newrelic_security/agent/configuration/manager.rb +65 -7
- data/lib/newrelic_security/agent/control/application_runtime_error.rb +1 -1
- data/lib/newrelic_security/agent/control/collector.rb +41 -4
- data/lib/newrelic_security/agent/control/control_command.rb +2 -3
- data/lib/newrelic_security/agent/control/error_reporting.rb +8 -6
- data/lib/newrelic_security/agent/control/event.rb +15 -1
- data/lib/newrelic_security/agent/control/event_processor.rb +25 -14
- data/lib/newrelic_security/agent/control/event_subscriber.rb +6 -8
- data/lib/newrelic_security/agent/control/health_check.rb +4 -0
- data/lib/newrelic_security/agent/control/http_context.rb +10 -6
- data/lib/newrelic_security/agent/control/iast_client.rb +24 -11
- data/lib/newrelic_security/agent/control/reflected_xss.rb +3 -4
- data/lib/newrelic_security/agent/control/scan_scheduler.rb +77 -0
- data/lib/newrelic_security/agent/control/websocket_client.rb +71 -16
- data/lib/newrelic_security/agent/utils/agent_utils.rb +25 -17
- data/lib/newrelic_security/constants.rb +1 -2
- data/lib/newrelic_security/instrumentation-security/async-http/instrumentation.rb +2 -13
- data/lib/newrelic_security/instrumentation-security/curb/instrumentation.rb +1 -14
- data/lib/newrelic_security/instrumentation-security/ethon/chain.rb +0 -6
- data/lib/newrelic_security/instrumentation-security/ethon/instrumentation.rb +7 -42
- data/lib/newrelic_security/instrumentation-security/ethon/prepend.rb +0 -4
- data/lib/newrelic_security/instrumentation-security/excon/instrumentation.rb +3 -13
- data/lib/newrelic_security/instrumentation-security/grape/instrumentation.rb +1 -0
- data/lib/newrelic_security/instrumentation-security/graphql/chain.rb +26 -0
- data/lib/newrelic_security/instrumentation-security/graphql/instrumentation.rb +28 -0
- data/lib/newrelic_security/instrumentation-security/graphql/prepend.rb +18 -0
- data/lib/newrelic_security/instrumentation-security/grpc/server/instrumentation.rb +3 -2
- data/lib/newrelic_security/instrumentation-security/httpclient/instrumentation.rb +4 -28
- data/lib/newrelic_security/instrumentation-security/httprb/instrumentation.rb +1 -12
- data/lib/newrelic_security/instrumentation-security/httpx/instrumentation.rb +1 -15
- data/lib/newrelic_security/instrumentation-security/instrumentation_utils.rb +0 -17
- data/lib/newrelic_security/instrumentation-security/io/chain.rb +2 -2
- data/lib/newrelic_security/instrumentation-security/io/prepend.rb +1 -1
- data/lib/newrelic_security/instrumentation-security/net_http/instrumentation.rb +6 -23
- data/lib/newrelic_security/instrumentation-security/net_ldap/instrumentation.rb +1 -1
- data/lib/newrelic_security/instrumentation-security/padrino/instrumentation.rb +1 -0
- data/lib/newrelic_security/instrumentation-security/patron/instrumentation.rb +2 -15
- data/lib/newrelic_security/instrumentation-security/rack/chain.rb +24 -0
- data/lib/newrelic_security/instrumentation-security/rack/instrumentation.rb +44 -0
- data/lib/newrelic_security/instrumentation-security/rack/prepend.rb +18 -0
- data/lib/newrelic_security/instrumentation-security/rails/instrumentation.rb +1 -0
- data/lib/newrelic_security/instrumentation-security/roda/instrumentation.rb +1 -0
- data/lib/newrelic_security/instrumentation-security/sinatra/instrumentation.rb +1 -0
- data/lib/newrelic_security/newrelic-security-api/api.rb +1 -1
- data/lib/newrelic_security/parse-cron/cron_parser.rb +294 -0
- data/lib/newrelic_security/version.rb +1 -1
- data/lib/newrelic_security/websocket-client-simple/client.rb +5 -1
- data/newrelic_security.gemspec +1 -1
- metadata +15 -7
@@ -9,7 +9,7 @@ module NewRelic::Security
|
|
9
9
|
|
10
10
|
class EventProcessor
|
11
11
|
|
12
|
-
attr_accessor :eventQ, :
|
12
|
+
attr_accessor :eventQ, :event_dequeue_threads, :healthcheck_thread
|
13
13
|
|
14
14
|
def initialize
|
15
15
|
@first_event = true
|
@@ -24,18 +24,22 @@ module NewRelic::Security
|
|
24
24
|
NewRelic::Security::Agent.init_logger.info "[STEP-3] => Gathering information about the application"
|
25
25
|
app_info = NewRelic::Security::Agent::Control::AppInfo.new
|
26
26
|
app_info.update_app_info
|
27
|
-
|
28
|
-
NewRelic::Security::Agent.
|
27
|
+
app_info_json = app_info.to_json
|
28
|
+
NewRelic::Security::Agent.logger.info "Sending application info : #{app_info_json}"
|
29
|
+
NewRelic::Security::Agent.init_logger.info "Sending application info : #{app_info_json}"
|
29
30
|
enqueue(app_info)
|
30
31
|
app_info = nil
|
32
|
+
app_info_json = nil
|
31
33
|
end
|
32
34
|
|
33
35
|
def send_application_url_mappings
|
34
36
|
application_url_mappings = NewRelic::Security::Agent::Control::ApplicationURLMappings.new
|
35
37
|
application_url_mappings.update_application_url_mappings
|
36
|
-
|
38
|
+
application_url_mappings_json = application_url_mappings.to_json
|
39
|
+
NewRelic::Security::Agent.logger.info "Sending application URL Mappings : #{application_url_mappings_json}"
|
37
40
|
enqueue(application_url_mappings)
|
38
41
|
application_url_mappings = nil
|
42
|
+
application_url_mappings_json = nil
|
39
43
|
end
|
40
44
|
|
41
45
|
def send_event(event)
|
@@ -48,6 +52,7 @@ module NewRelic::Security
|
|
48
52
|
enqueue(event)
|
49
53
|
if @first_event
|
50
54
|
NewRelic::Security::Agent.init_logger.info "[STEP-8] => First event sent for validation. Security agent started successfully : #{event.to_json}"
|
55
|
+
NewRelic::Security::Agent.config.traffic_start_time = current_time_millis unless NewRelic::Security::Agent.config[:traffic_start_time]
|
51
56
|
@first_event = false
|
52
57
|
end
|
53
58
|
event = nil
|
@@ -64,7 +69,7 @@ module NewRelic::Security
|
|
64
69
|
if exc
|
65
70
|
exception = {}
|
66
71
|
exception[:message] = exc.message
|
67
|
-
exception[:cause] = exc.cause
|
72
|
+
exception[:cause] = { :message => exc.cause }
|
68
73
|
exception[:stackTrace] = exc.backtrace.map(&:to_s)
|
69
74
|
end
|
70
75
|
critical_message = NewRelic::Security::Agent::Control::CriticalMessage.new(message, level, caller, thread_name, exception)
|
@@ -86,15 +91,17 @@ module NewRelic::Security
|
|
86
91
|
private
|
87
92
|
|
88
93
|
def create_dequeue_threads
|
89
|
-
|
90
|
-
|
91
|
-
Thread.
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
@event_dequeue_threads = []
|
95
|
+
3.times do |t|
|
96
|
+
@event_dequeue_threads<< Thread.new do
|
97
|
+
Thread.current.name = "newrelic_security_event_thread-#{t}"
|
98
|
+
loop do
|
99
|
+
begin
|
100
|
+
data_to_be_sent = @eventQ.pop
|
101
|
+
NewRelic::Security::Agent::Control::WebsocketClient.instance.send(data_to_be_sent)
|
102
|
+
rescue => exception
|
103
|
+
NewRelic::Security::Agent.logger.error "Exception in event pop operation : #{exception.inspect}"
|
104
|
+
end
|
98
105
|
end
|
99
106
|
end
|
100
107
|
end
|
@@ -130,6 +137,10 @@ module NewRelic::Security
|
|
130
137
|
NewRelic::Security::Agent.logger.error "Exception in health check thread, #{exception.inspect}"
|
131
138
|
end
|
132
139
|
|
140
|
+
def current_time_millis
|
141
|
+
(Time.now.to_f * 1000).to_i
|
142
|
+
end
|
143
|
+
|
133
144
|
def create_error_reporting_thread
|
134
145
|
@error_reporting_thread = Thread.new {
|
135
146
|
Thread.current.name = "newrelic_security_error_reporting_thread"
|
@@ -7,19 +7,17 @@ module NewRelic::Security
|
|
7
7
|
NewRelic::Security::Agent.logger.info "NewRelic server_source_configuration_added for pid : #{Process.pid}, Parent Pid : #{Process.ppid}"
|
8
8
|
NewRelic::Security::Agent.init_logger.info "NewRelic server_source_configuration_added for pid : #{Process.pid}, Parent Pid : #{Process.ppid}"
|
9
9
|
NewRelic::Security::Agent.config.update_server_config
|
10
|
-
if NewRelic::Security::Agent.config[:enabled] && !NewRelic::Security::Agent.config[:high_security]
|
11
|
-
NewRelic::Security::Agent.agent.
|
10
|
+
if NewRelic::Security::Agent.config[:'security.enabled'] && !NewRelic::Security::Agent.config[:high_security]
|
11
|
+
NewRelic::Security::Agent.agent.event_processor&.event_dequeue_threads&.each { |t| t&.kill }
|
12
|
+
NewRelic::Security::Agent.agent.event_processor = nil
|
13
|
+
@csec_agent_main_thread&.kill
|
14
|
+
@csec_agent_main_thread = nil
|
15
|
+
@csec_agent_main_thread = Thread.new { NewRelic::Security::Agent.agent.scan_scheduler.init_via_scan_scheduler }
|
12
16
|
else
|
13
17
|
NewRelic::Security::Agent.logger.info "New Relic Security is disabled by one of the user provided config `security.enabled` or `high_security`."
|
14
18
|
NewRelic::Security::Agent.init_logger.info "New Relic Security is disabled by one of the user provided config `security.enabled` or `high_security`."
|
15
19
|
end
|
16
20
|
}
|
17
|
-
::NewRelic::Agent.instance.events.subscribe(:security_policy_received) { |received_policy|
|
18
|
-
NewRelic::Security::Agent.logger.info "security_policy_received pid ::::::: #{Process.pid} #{Process.ppid}, #{received_policy}"
|
19
|
-
NewRelic::Security::Agent.config[:policy].merge!(received_policy)
|
20
|
-
NewRelic::Security::Agent.init_logger.info "[STEP-7] => Received and applied policy/configuration : #{received_policy}"
|
21
|
-
NewRelic::Security::Agent.agent.start_iast_client if NewRelic::Security::Agent::Utils.is_IAST?
|
22
|
-
}
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
@@ -35,6 +35,10 @@ module NewRelic::Security
|
|
35
35
|
@iastEventStats = {}
|
36
36
|
@raspEventStats = {}
|
37
37
|
@exitEventStats = {}
|
38
|
+
@procStartTime = NewRelic::Security::Agent.config[:process_start_time]
|
39
|
+
@trafficStartedTime = NewRelic::Security::Agent.config[:traffic_start_time]
|
40
|
+
@scanStartTime = NewRelic::Security::Agent.config[:scan_start_time]
|
41
|
+
@iastTestIdentifer = NewRelic::Security::Agent.config[:'security.iast_test_identifier']
|
38
42
|
end
|
39
43
|
|
40
44
|
def as_json
|
@@ -12,17 +12,20 @@ module NewRelic::Security
|
|
12
12
|
REQUEST_METHOD = 'REQUEST_METHOD'
|
13
13
|
HTTP_HOST = 'HTTP_HOST'
|
14
14
|
PATH_INFO = 'PATH_INFO'
|
15
|
+
QUERY_STRING = 'QUERY_STRING'
|
15
16
|
RACK_INPUT = 'rack.input'
|
16
17
|
CGI_VARIABLES = ::Set.new(%W[ AUTH_TYPE CONTENT_LENGTH CONTENT_TYPE GATEWAY_INTERFACE HTTPS HTTP_HOST PATH_INFO PATH_TRANSLATED REQUEST_URI QUERY_STRING REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER REQUEST_METHOD SCRIPT_NAME SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE rack.url_scheme ])
|
18
|
+
REQUEST_BODY_LIMIT = 500 #KB
|
17
19
|
|
18
20
|
class HTTPContext
|
19
21
|
|
20
|
-
attr_accessor :time_stamp, :req, :method, :headers, :params, :body, :data_truncated, :route, :cache, :fuzz_files, :event_counter, :mutex
|
22
|
+
attr_accessor :time_stamp, :req, :method, :headers, :params, :body, :data_truncated, :route, :cache, :fuzz_files, :event_counter, :custom_data_type, :mutex, :url
|
21
23
|
|
22
24
|
def initialize(env)
|
23
25
|
@time_stamp = current_time_millis
|
24
26
|
@req = env.select { |key, _| CGI_VARIABLES.include? key}
|
25
27
|
@method = @req[REQUEST_METHOD]
|
28
|
+
@url = "#{@req[PATH_INFO]}?#{@req[QUERY_STRING]}"
|
26
29
|
@headers = env.select { |key, _| key.include?(HTTP_) }
|
27
30
|
@headers = @headers.transform_keys{ |key| key[5..-1].gsub(UNDERSCORE, HYPHEN).downcase }
|
28
31
|
request = Rack::Request.new(env) unless env.empty?
|
@@ -31,20 +34,21 @@ module NewRelic::Security
|
|
31
34
|
strio = env[RACK_INPUT]
|
32
35
|
if strio.instance_of?(::StringIO)
|
33
36
|
offset = strio.tell
|
34
|
-
@body = strio.read(
|
37
|
+
@body = strio.read(REQUEST_BODY_LIMIT * 1024) #after read, offset changes
|
35
38
|
strio.seek(offset)
|
36
39
|
# In case of Grape and Roda strio.read giving empty result, added below approach to handle such cases
|
37
40
|
@body = strio.string if @body.nil? && strio.size > 0
|
38
41
|
elsif defined?(::Rack) && defined?(::Rack::Lint::InputWrapper) && strio.instance_of?(::Rack::Lint::InputWrapper)
|
39
|
-
@body = strio.read(
|
42
|
+
@body = strio.read(REQUEST_BODY_LIMIT * 1024)
|
40
43
|
elsif defined?(::Protocol::Rack::Input) && defined?(::Protocol::Rack::Input) && strio.instance_of?(::Protocol::Rack::Input)
|
41
|
-
@body = strio.read(
|
44
|
+
@body = strio.read(REQUEST_BODY_LIMIT * 1024)
|
42
45
|
elsif defined?(::PhusionPassenger::Utils::TeeInput) && strio.instance_of?(::PhusionPassenger::Utils::TeeInput)
|
43
|
-
@body = strio.read(
|
46
|
+
@body = strio.read(REQUEST_BODY_LIMIT * 1024)
|
44
47
|
end
|
45
|
-
@data_truncated = @body && @body.size >=
|
48
|
+
@data_truncated = @body && @body.size >= REQUEST_BODY_LIMIT * 1024
|
46
49
|
strio&.rewind
|
47
50
|
@body = @body.force_encoding(Encoding::UTF_8) if @body.is_a?(String)
|
51
|
+
@custom_data_type = {}
|
48
52
|
@cache = Hash.new
|
49
53
|
@fuzz_files = ::Set.new
|
50
54
|
@event_counter = 0
|
@@ -17,9 +17,8 @@ module NewRelic::Security
|
|
17
17
|
IS_GRPC = 'isGrpc'
|
18
18
|
INPUT_CLASS = 'inputClass'
|
19
19
|
SERVER_PORT_1 = 'serverPort'
|
20
|
-
PROBING = 'probing'
|
21
|
-
INTERVAL = 'interval'
|
22
20
|
IS_GRPC_CLIENT_STREAM = 'isGrpcClientStream'
|
21
|
+
PROBING_INTERVAL = 5
|
23
22
|
|
24
23
|
class IASTClient
|
25
24
|
|
@@ -54,6 +53,7 @@ module NewRelic::Security
|
|
54
53
|
Thread.current.name = "newrelic_security_iast_thread-#{t}"
|
55
54
|
loop do
|
56
55
|
fuzz_request = @fuzzQ.deq #thread blocks when the queue is empty
|
56
|
+
NewRelic::Security::Agent.config.scan_start_time = current_time_millis unless NewRelic::Security::Agent.config[:scan_start_time]
|
57
57
|
if fuzz_request.request[IS_GRPC]
|
58
58
|
fire_grpc_request(fuzz_request.id, fuzz_request.request, fuzz_request.reflected_metadata)
|
59
59
|
else
|
@@ -71,7 +71,8 @@ module NewRelic::Security
|
|
71
71
|
@iast_data_transfer_request_processor_thread = Thread.new do
|
72
72
|
Thread.current.name = "newrelic_security_iast_data_transfer_request_processor"
|
73
73
|
loop do
|
74
|
-
|
74
|
+
# TODO: Check & remove this probing interval if not required, earlier this was used from policy sent by SE.
|
75
|
+
sleep PROBING_INTERVAL
|
75
76
|
current_timestamp = current_time_millis
|
76
77
|
cooldown_sleep_time = @cooldown_till_timestamp - current_timestamp
|
77
78
|
sleep cooldown_sleep_time/1000 if cooldown_sleep_time > 0
|
@@ -84,9 +85,21 @@ module NewRelic::Security
|
|
84
85
|
if batch_size > 100 && remaining_record_capacity > batch_size
|
85
86
|
iast_data_transfer_request = NewRelic::Security::Agent::Control::IASTDataTransferRequest.new
|
86
87
|
iast_data_transfer_request.batchSize = batch_size * 2
|
88
|
+
# TODO: Below calculation of batch_size overrides above logic and can be removed once below one is stablises or rate limit feature is released.
|
89
|
+
if NewRelic::Security::Agent.config[:'security.scan_controllers.iast_scan_request_rate_limit']
|
90
|
+
batch_size =
|
91
|
+
if NewRelic::Security::Agent.config[:'security.scan_controllers.iast_scan_request_rate_limit'] < 12
|
92
|
+
1
|
93
|
+
elsif NewRelic::Security::Agent.config[:'security.scan_controllers.iast_scan_request_rate_limit'] > 3600
|
94
|
+
300
|
95
|
+
else
|
96
|
+
NewRelic::Security::Agent.config[:'security.scan_controllers.iast_scan_request_rate_limit'] / 12
|
97
|
+
end
|
98
|
+
iast_data_transfer_request.batchSize = batch_size
|
99
|
+
end
|
87
100
|
iast_data_transfer_request.pendingRequestIds = pending_request_ids.to_a
|
88
101
|
iast_data_transfer_request.completedRequests = completed_requests
|
89
|
-
NewRelic::Security::Agent.agent.event_processor
|
102
|
+
NewRelic::Security::Agent.agent.event_processor&.send_iast_data_transfer_request(iast_data_transfer_request) if NewRelic::Security::Agent::Control::WebsocketClient.instance.is_open?
|
90
103
|
end
|
91
104
|
end
|
92
105
|
end
|
@@ -122,18 +135,18 @@ module NewRelic::Security
|
|
122
135
|
def fire_grpc_request(fuzz_request_id, request, reflected_metadata)
|
123
136
|
service = Object.const_get(request[METHOD].split(SLASH)[0]).superclass
|
124
137
|
method = request[METHOD].split(SLASH)[1]
|
125
|
-
@stub
|
138
|
+
@stub ||= service.rpc_stub_class.new("localhost:#{request[SERVER_PORT_1]}", :this_channel_is_insecure)
|
126
139
|
|
127
|
-
parsed_body =
|
128
|
-
if reflected_metadata[IS_GRPC_CLIENT_STREAM]
|
129
|
-
|
140
|
+
parsed_body = request[BODY][1..-2].split(',')
|
141
|
+
chunks_enum = if reflected_metadata[IS_GRPC_CLIENT_STREAM]
|
142
|
+
Enumerator.new do |y|
|
130
143
|
parsed_body.each do |b|
|
131
144
|
y << Object.const_get(reflected_metadata[INPUT_CLASS]).decode_json(b)
|
132
145
|
end
|
133
146
|
end
|
134
|
-
|
135
|
-
|
136
|
-
|
147
|
+
else
|
148
|
+
Object.const_get(reflected_metadata[INPUT_CLASS]).decode_json(request[BODY])
|
149
|
+
end
|
137
150
|
response = @stub.public_send(method, chunks_enum, metadata: request[HEADERS])
|
138
151
|
# response = @stub.send(method, JSON.parse(request['body'], object_class: OpenStruct))
|
139
152
|
# request[HEADERS].delete(VERSION) if request[HEADERS].key?(VERSION)
|
@@ -108,8 +108,8 @@ module NewRelic::Security
|
|
108
108
|
processed_data.add(body)
|
109
109
|
end
|
110
110
|
when APPLICATION_XML
|
111
|
-
|
112
|
-
processed_data.add(
|
111
|
+
xml_data = ::CGI.unescapeHTML(body)
|
112
|
+
processed_data.add(xml_data)
|
113
113
|
when APPLICATION_X_WWW_FORM_URLENCODED
|
114
114
|
body = ::CGI.unescape(body, UTF_8)
|
115
115
|
processed_data.add(body)
|
@@ -134,7 +134,7 @@ module NewRelic::Security
|
|
134
134
|
# do while loop in java code here
|
135
135
|
old_processed_body = processed_body
|
136
136
|
body = ::JSON.parse(processed_body)
|
137
|
-
processed_data.add(body) if old_processed_body != body && body.to_s.include?(LESS_THAN)
|
137
|
+
processed_data.add(body.to_s) if old_processed_body != body && body.to_s.include?(LESS_THAN)
|
138
138
|
when APPLICATION_XML
|
139
139
|
# Unescaping of xml data is remaining
|
140
140
|
processed_data.add(processed_data)
|
@@ -176,7 +176,6 @@ module NewRelic::Security
|
|
176
176
|
start_pos = 0
|
177
177
|
tmp_curr_pos = 0
|
178
178
|
tmp_start_pos = 0
|
179
|
-
|
180
179
|
while curr_pos < data.length
|
181
180
|
matcher = TAG_NAME_REGEX.match(data, curr_pos)
|
182
181
|
is_attack_construct = false
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'newrelic_security/parse-cron/cron_parser'
|
2
|
+
|
3
|
+
module NewRelic::Security
|
4
|
+
module Agent
|
5
|
+
module Control
|
6
|
+
class ScanScheduler
|
7
|
+
def init_via_scan_scheduler
|
8
|
+
if NewRelic::Security::Agent.config[:'security.scan_schedule.delay'].positive?
|
9
|
+
NewRelic::Security::Agent.logger.info "IAST delay is set to: #{NewRelic::Security::Agent.config[:'security.scan_schedule.delay']}, current time: #{Time.now}"
|
10
|
+
start_agent_with_delay(NewRelic::Security::Agent.config[:'security.scan_schedule.delay']*60)
|
11
|
+
elsif !NewRelic::Security::Agent.config[:'security.scan_schedule.schedule'].to_s.empty?
|
12
|
+
cron_expression_task(NewRelic::Security::Agent.config[:'security.scan_schedule.schedule'], NewRelic::Security::Agent.config[:'security.scan_schedule.duration']*60)
|
13
|
+
else
|
14
|
+
NewRelic::Security::Agent.agent.init
|
15
|
+
NewRelic::Security::Agent.agent.start_iast_client if NewRelic::Security::Agent::Utils.is_IAST?
|
16
|
+
shutdown_at_duration_reached(NewRelic::Security::Agent.config[:'security.scan_schedule.duration']*60)
|
17
|
+
end
|
18
|
+
rescue StandardError => exception
|
19
|
+
NewRelic::Security::Agent.logger.error "Exception in IAST scan scheduler: #{exception.inspect} #{exception.backtrace}"
|
20
|
+
::NewRelic::Agent.notice_error(exception)
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_agent_with_delay(delay)
|
24
|
+
NewRelic::Security::Agent.logger.info "Security Agent delay scan time is set to: #{(delay/60).ceil} minutes when always_sample_traces is #{NewRelic::Security::Agent.config[:'security.scan_schedule.always_sample_traces']}, current time: #{Time.now}"
|
25
|
+
if NewRelic::Security::Agent.config[:'security.scan_schedule.always_sample_traces']
|
26
|
+
NewRelic::Security::Agent.agent.init unless NewRelic::Security::Agent.config[:enabled]
|
27
|
+
sleep delay if NewRelic::Security::Agent.config[:'security.scan_schedule.always_sample_traces']
|
28
|
+
else
|
29
|
+
sleep delay
|
30
|
+
NewRelic::Security::Agent.agent.init
|
31
|
+
end
|
32
|
+
NewRelic::Security::Agent.agent.start_iast_client if NewRelic::Security::Agent::Utils.is_IAST?
|
33
|
+
shutdown_at_duration_reached(NewRelic::Security::Agent.config[:'security.scan_schedule.duration']*60)
|
34
|
+
end
|
35
|
+
|
36
|
+
def shutdown_at_duration_reached(duration)
|
37
|
+
shutdown_at = Time.now.to_i + duration
|
38
|
+
shut_down_time = (Time.now + duration).strftime("%a %d %b %Y %H:%M:%S")
|
39
|
+
return if duration <= 0
|
40
|
+
NewRelic::Security::Agent.logger.info "IAST Duration is set to: #{duration/60} minutes, timestamp: #{shut_down_time} time, current time: #{Time.now}"
|
41
|
+
@shutdown_monitor_thread = Thread.new do
|
42
|
+
Thread.current.name = "newrelic_security_shutdown_monitor_thread"
|
43
|
+
loop do
|
44
|
+
sleep 1
|
45
|
+
next if Time.now.to_i < shutdown_at
|
46
|
+
if NewRelic::Security::Agent.config[:'security.scan_schedule.always_sample_traces']
|
47
|
+
NewRelic::Security::Agent.logger.info "Shutdown IAST Data transfer request processor only as 'security.scan_schedule.always_sample_traces' is #{NewRelic::Security::Agent.config[:'security.scan_schedule.always_sample_traces']} now at current time: #{Time.now}"
|
48
|
+
NewRelic::Security::Agent.agent.iast_client&.iast_data_transfer_request_processor_thread&.kill
|
49
|
+
else
|
50
|
+
NewRelic::Security::Agent.logger.info "Shutdown IAST agent now at current time: #{Time.now}"
|
51
|
+
::NewRelic::Agent.notice_error(StandardError.new("WS Connection closed by local"))
|
52
|
+
NewRelic::Security::Agent.agent.shutdown_security_agent
|
53
|
+
end
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def cron_expression_task(schedule, duration)
|
60
|
+
@cron_parser = NewRelic::Security::ParseCron::CronParser.new(schedule)
|
61
|
+
loop do
|
62
|
+
next_run = @cron_parser.next(Time.now)
|
63
|
+
NewRelic::Security::Agent.logger.info "Next init via cron exp: #{schedule}, is scheduled at : #{next_run}"
|
64
|
+
delay = next_run - Time.now
|
65
|
+
if NewRelic::Security::Agent.agent.iast_client&.iast_data_transfer_request_processor_thread&.alive?
|
66
|
+
sleep delay > 2 ? delay - 2 : delay
|
67
|
+
else
|
68
|
+
start_agent_with_delay(delay)
|
69
|
+
end
|
70
|
+
return if duration <= 0
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -21,6 +21,10 @@ module NewRelic::Security
|
|
21
21
|
NR_CSEC_ENTITY_NAME = 'NR-CSEC-ENTITY-NAME'
|
22
22
|
NR_CSEC_ENTITY_GUID = 'NR-CSEC-ENTITY-GUID'
|
23
23
|
NR_CSEC_IAST_DATA_TRANSFER_MODE = 'NR-CSEC-IAST-DATA-TRANSFER-MODE'
|
24
|
+
NR_CSEC_IGNORED_VUL_CATEGORIES = 'NR-CSEC-IGNORED-VUL-CATEGORIES'
|
25
|
+
NR_CSEC_PROCESS_START_TIME = 'NR-CSEC-PROCESS-START-TIME'
|
26
|
+
NR_CSEC_IAST_SCAN_INSTANCE_COUNT = 'NR-CSEC-IAST-SCAN-INSTANCE-COUNT'
|
27
|
+
NR_CSEC_IAST_TEST_IDENTIFIER = 'NR-CSEC-IAST-TEST-IDENTIFIER'
|
24
28
|
|
25
29
|
class WebsocketClient
|
26
30
|
include Singleton
|
@@ -43,6 +47,13 @@ module NewRelic::Security
|
|
43
47
|
headers[NR_CSEC_ENTITY_NAME] = NewRelic::Security::Agent.config[:app_name]
|
44
48
|
headers[NR_CSEC_ENTITY_GUID] = NewRelic::Security::Agent.config[:entity_guid]
|
45
49
|
headers[NR_CSEC_IAST_DATA_TRANSFER_MODE] = PULL
|
50
|
+
headers[NR_CSEC_IGNORED_VUL_CATEGORIES] = ingnored_vul_categories.join(COMMA)
|
51
|
+
headers[NR_CSEC_PROCESS_START_TIME] = NewRelic::Security::Agent.config[:process_start_time]
|
52
|
+
headers[NR_CSEC_IAST_SCAN_INSTANCE_COUNT] = NewRelic::Security::Agent.config[:'security.scan_controllers.scan_instance_count']
|
53
|
+
if NewRelic::Security::Agent.config[:'security.iast_test_identifier'] && !NewRelic::Security::Agent.config[:'security.iast_test_identifier'].empty?
|
54
|
+
headers[NR_CSEC_IAST_TEST_IDENTIFIER] = NewRelic::Security::Agent.config[:'security.iast_test_identifier']
|
55
|
+
headers[NR_CSEC_IAST_SCAN_INSTANCE_COUNT] = 1
|
56
|
+
end
|
46
57
|
|
47
58
|
begin
|
48
59
|
cert_store = ::OpenSSL::X509::Store.new
|
@@ -50,13 +61,16 @@ module NewRelic::Security
|
|
50
61
|
NewRelic::Security::Agent.logger.info "Websocket connection URL : #{NewRelic::Security::Agent.config[:validator_service_url]}"
|
51
62
|
connection = NewRelic::Security::WebSocket::Client::Simple.connect NewRelic::Security::Agent.config[:validator_service_url], headers: headers, cert_store: cert_store
|
52
63
|
@ws = connection
|
64
|
+
@mutex = Mutex.new
|
53
65
|
|
54
66
|
connection.on :open do
|
67
|
+
headers = nil
|
55
68
|
NewRelic::Security::Agent.logger.debug "Websocket connected with IC, AgentEventMachine #{NewRelic::Security::Agent::Utils.filtered_log(connection.inspect)}"
|
56
69
|
NewRelic::Security::Agent.init_logger.info "[STEP-4] => Web socket connection to SaaS validator established successfully"
|
57
70
|
NewRelic::Security::Agent.agent.event_processor.send_app_info
|
58
71
|
NewRelic::Security::Agent.agent.event_processor.send_application_url_mappings
|
59
72
|
NewRelic::Security::Agent.config.enable_security
|
73
|
+
NewRelic::Security::Agent::Control::WebsocketClient.instance.start_ping_thread
|
60
74
|
end
|
61
75
|
|
62
76
|
connection.on :message do |msg|
|
@@ -71,13 +85,14 @@ module NewRelic::Security
|
|
71
85
|
connection.on :close do |e|
|
72
86
|
NewRelic::Security::Agent.logger.info "Closing websocket connection : #{e.inspect}\n"
|
73
87
|
NewRelic::Security::Agent.config.disable_security
|
74
|
-
|
88
|
+
reconnect_interval = e.instance_of?(TrueClass) ? 0 : 15
|
89
|
+
Thread.new { NewRelic::Security::Agent.agent.reconnect(reconnect_interval) } if e
|
75
90
|
end
|
76
91
|
|
77
92
|
connection.on :error do |e|
|
78
93
|
NewRelic::Security::Agent.logger.error "Error in websocket connection : #{e.inspect} #{e.backtrace}"
|
79
94
|
::NewRelic::Agent.notice_error(e)
|
80
|
-
Thread.new { NewRelic::Security::Agent::Control::WebsocketClient.instance.close(
|
95
|
+
Thread.new { NewRelic::Security::Agent::Control::WebsocketClient.instance.close(e) }
|
81
96
|
end
|
82
97
|
rescue Errno::EPIPE => exception
|
83
98
|
NewRelic::Security::Agent.logger.error "Unable to connect to validator_service: #{exception.inspect}"
|
@@ -103,25 +118,37 @@ module NewRelic::Security
|
|
103
118
|
end
|
104
119
|
|
105
120
|
def send(message)
|
106
|
-
message_json =
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
121
|
+
message_json = nil
|
122
|
+
begin
|
123
|
+
message_json = message.to_json
|
124
|
+
NewRelic::Security::Agent.logger.debug "Sending #{message.jsonName} : #{message_json}"
|
125
|
+
@mutex.synchronize do
|
126
|
+
res = @ws.send(message_json)
|
127
|
+
if res && message.jsonName == :Event
|
128
|
+
NewRelic::Security::Agent.agent.event_sent_count.increment
|
129
|
+
if NewRelic::Security::Agent::Utils.is_IAST_request?(message.httpRequest[:headers])
|
130
|
+
NewRelic::Security::Agent.agent.iast_event_stats.sent.increment
|
131
|
+
else
|
132
|
+
NewRelic::Security::Agent.agent.rasp_event_stats.sent.increment
|
133
|
+
end
|
134
|
+
end
|
135
|
+
NewRelic::Security::Agent.agent.exit_event_stats.sent.increment if res && message.jsonName == :'exit-event'
|
115
136
|
end
|
137
|
+
rescue Exception => exception
|
138
|
+
NewRelic::Security::Agent.logger.error "Exception in sending message : #{exception.inspect} #{exception.backtrace}, message: #{message_json}"
|
139
|
+
NewRelic::Security::Agent.agent.event_drop_count.increment if message.jsonName == :Event
|
140
|
+
NewRelic::Security::Agent.agent.event_processor.send_critical_message(exception.message, "SEVERE", caller_locations[0].to_s, Thread.current.name, exception)
|
141
|
+
ensure
|
142
|
+
message_json = nil
|
116
143
|
end
|
117
|
-
NewRelic::Security::Agent.agent.exit_event_stats.sent.increment if res && message.jsonName == :'exit-event'
|
118
|
-
rescue Exception => exception
|
119
|
-
NewRelic::Security::Agent.logger.error "Exception in sending message : #{exception.inspect} #{exception.backtrace}"
|
120
|
-
NewRelic::Security::Agent.agent.event_drop_count.increment if message.jsonName == :Event
|
121
|
-
NewRelic::Security::Agent.agent.event_processor.send_critical_message(exception.message, "SEVERE", caller_locations[0].to_s, Thread.current.name, exception)
|
122
144
|
end
|
123
145
|
|
124
146
|
def close(reconnect = true)
|
147
|
+
NewRelic::Security::Agent.config.disable_security
|
148
|
+
NewRelic::Security::Agent.logger.info "Flushing eventQ (#{NewRelic::Security::Agent.agent.event_processor.eventQ.size} events) and closing websocket connection"
|
149
|
+
NewRelic::Security::Agent.agent.event_processor&.eventQ&.clear
|
150
|
+
@iast_client&.iast_data_transfer_request_processor_thread&.kill
|
151
|
+
stop_ping_thread
|
125
152
|
@ws.close(reconnect) if @ws
|
126
153
|
end
|
127
154
|
|
@@ -130,6 +157,34 @@ module NewRelic::Security
|
|
130
157
|
false
|
131
158
|
end
|
132
159
|
|
160
|
+
def start_ping_thread
|
161
|
+
@ping_thread = Thread.new do
|
162
|
+
loop do
|
163
|
+
sleep 30
|
164
|
+
@ws.send(EMPTY_STRING, :type => :ping)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def stop_ping_thread
|
172
|
+
@ping_thread&.kill
|
173
|
+
@ping_thread = nil
|
174
|
+
end
|
175
|
+
|
176
|
+
def ingnored_vul_categories
|
177
|
+
list = []
|
178
|
+
list << FILE_OPERATION << FILE_INTEGRITY if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.invalid_file_access']
|
179
|
+
list << SQL_DB_COMMAND if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.sql_injection']
|
180
|
+
list << NOSQL_DB_COMMAND if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.nosql_injection']
|
181
|
+
list << LDAP if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ldap_injection']
|
182
|
+
list << SYSTEM_COMMAND if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.command_injection']
|
183
|
+
list << XPATH if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.xpath_injection']
|
184
|
+
list << HTTP_REQUEST if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ssrf']
|
185
|
+
list << REFLECTED_XSS if NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.rxss']
|
186
|
+
list
|
187
|
+
end
|
133
188
|
end
|
134
189
|
end
|
135
190
|
end
|
@@ -15,8 +15,7 @@ module NewRelic::Security
|
|
15
15
|
ASTERISK = '*'
|
16
16
|
|
17
17
|
def is_IAST?
|
18
|
-
return
|
19
|
-
return NewRelic::Security::Agent.config[:policy][VULNERABILITY_SCAN][IAST_SCAN][ENABLED] if NewRelic::Security::Agent.config[:policy][VULNERABILITY_SCAN][ENABLED]
|
18
|
+
return true if NewRelic::Security::Agent.config[:mode] == IAST
|
20
19
|
false
|
21
20
|
end
|
22
21
|
|
@@ -96,7 +95,8 @@ module NewRelic::Security
|
|
96
95
|
|
97
96
|
def get_app_routes(framework, router = nil)
|
98
97
|
enable_object_space_in_jruby
|
99
|
-
|
98
|
+
case framework
|
99
|
+
when :rails
|
100
100
|
::Rails.application.routes.routes.each do |route|
|
101
101
|
if route.verb.is_a?(::Regexp)
|
102
102
|
method = route.verb.inspect.match(/[a-zA-Z]+/)
|
@@ -107,33 +107,41 @@ module NewRelic::Security
|
|
107
107
|
}
|
108
108
|
end
|
109
109
|
end
|
110
|
-
|
110
|
+
when :sinatra
|
111
111
|
::Sinatra::Application.routes.each do |method, routes|
|
112
112
|
routes.map { |r| r.first.to_s }.map do |route|
|
113
113
|
NewRelic::Security::Agent.agent.route_map << "#{method}@#{route}"
|
114
114
|
end
|
115
115
|
end
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
116
|
+
when :grape
|
117
|
+
if defined?(::Grape::API)
|
118
|
+
ObjectSpace.each_object(Class).select { |klass| klass < ::Grape::API }.each do |api_class|
|
119
|
+
api_class.routes.each do |route|
|
120
|
+
http_method = route.request_method || route.options[:method]
|
121
|
+
NewRelic::Security::Agent.agent.route_map << "#{http_method}@#{route.pattern.origin}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
when :padrino
|
124
126
|
if router.instance_of?(::Padrino::PathRouter::Router)
|
125
127
|
router.instance_variable_get(:@routes).each do |route|
|
126
128
|
NewRelic::Security::Agent.agent.route_map << "#{route.instance_variable_get(:@verb)}@#{route.matcher.instance_variable_get(:@path)}"
|
127
129
|
end
|
128
130
|
end
|
129
|
-
|
130
|
-
NewRelic::Security::Agent.logger.
|
131
|
+
when :roda
|
132
|
+
NewRelic::Security::Agent.logger.debug "TODO: Roda is a routing tree web toolkit, which generates route dynamically, hence route extraction is not possible."
|
133
|
+
when :grpc
|
134
|
+
router.owner.superclass.public_instance_methods(false).each do |m|
|
135
|
+
NewRelic::Security::Agent.agent.route_map << "*@/#{router.owner}/#{m}"
|
136
|
+
end
|
137
|
+
when :rack
|
138
|
+
# TODO: API enpointes(routes) extraction for rack
|
131
139
|
else
|
132
140
|
NewRelic::Security::Agent.logger.error "Unable to get app routes as Framework not detected"
|
133
141
|
end
|
134
142
|
disable_object_space_in_jruby if NewRelic::Security::Agent.config[:jruby_objectspace_enabled]
|
135
143
|
NewRelic::Security::Agent.logger.debug "ALL ROUTES : #{NewRelic::Security::Agent.agent.route_map}"
|
136
|
-
NewRelic::Security::Agent.agent.event_processor&.send_application_url_mappings
|
144
|
+
NewRelic::Security::Agent.agent.event_processor&.send_application_url_mappings unless NewRelic::Security::Agent.agent.route_map.empty?
|
137
145
|
rescue Exception => exception
|
138
146
|
NewRelic::Security::Agent.logger.error "Error in get app routes : #{exception.inspect} #{exception.backtrace}"
|
139
147
|
end
|
@@ -195,14 +203,14 @@ module NewRelic::Security
|
|
195
203
|
end
|
196
204
|
|
197
205
|
def enable_object_space_in_jruby
|
198
|
-
if RUBY_ENGINE == 'jruby' && !JRuby.objectspace
|
206
|
+
if RUBY_ENGINE == 'jruby' && JRuby.respond_to?(:objectspace) && !JRuby.objectspace
|
199
207
|
JRuby.objectspace = true
|
200
208
|
NewRelic::Security::Agent.config.jruby_objectspace_enabled = true
|
201
209
|
end
|
202
210
|
end
|
203
211
|
|
204
212
|
def disable_object_space_in_jruby
|
205
|
-
if RUBY_ENGINE == 'jruby' && JRuby.objectspace
|
213
|
+
if RUBY_ENGINE == 'jruby' && JRuby.respond_to?(:objectspace) && JRuby.objectspace
|
206
214
|
JRuby.objectspace = false
|
207
215
|
NewRelic::Security::Agent.config.jruby_objectspace_enabled = false
|
208
216
|
end
|
@@ -17,6 +17,7 @@ module NewRelic::Security
|
|
17
17
|
NR_CSEC_FUZZ_REQUEST_ID = 'nr-csec-fuzz-request-id'
|
18
18
|
NR_CSEC_TRACING_DATA = 'nr-csec-tracing-data'
|
19
19
|
NR_CSEC_PARENT_ID = 'nr-csec-parent-id'
|
20
|
+
IAST = 'IAST'
|
20
21
|
COLON_IAST_COLON = ':IAST:'
|
21
22
|
NOSQL_DB_COMMAND = 'NOSQL_DB_COMMAND'
|
22
23
|
SQL_DB_COMMAND = 'SQL_DB_COMMAND'
|
@@ -63,6 +64,4 @@ module NewRelic::Security
|
|
63
64
|
CONTENT_TYPE1 = 'content-Type'
|
64
65
|
PULL = 'PULL'
|
65
66
|
SHA1 = 'sha1'
|
66
|
-
VULNERABILITY_SCAN = 'vulnerabilityScan'
|
67
|
-
IAST_SCAN = 'iastScan'
|
68
67
|
end
|
@@ -6,22 +6,11 @@ module NewRelic::Security
|
|
6
6
|
module Instrumentation
|
7
7
|
module AsyncHttp
|
8
8
|
|
9
|
-
def call_on_enter(
|
9
|
+
def call_on_enter(_method, url, headers, _body)
|
10
10
|
event = nil
|
11
11
|
NewRelic::Security::Agent.logger.debug "OnEnter : #{self.class}.#{__method__}"
|
12
|
-
ob = {}
|
13
|
-
ob[:Method] = method
|
14
12
|
uri = ::URI.parse url
|
15
|
-
|
16
|
-
ob[:host] = uri.host
|
17
|
-
ob[:port] = uri.port
|
18
|
-
ob[:URI] = uri.to_s
|
19
|
-
ob[:path] = uri.path
|
20
|
-
ob[:query] = uri.query
|
21
|
-
ob[:Body] = body.respond_to?(:join) ? body.join.to_s : body.to_s
|
22
|
-
ob[:Headers] = headers.to_h
|
23
|
-
ob.each { |_, value| value.dup.force_encoding(ISO_8859_1).encode(UTF_8) if value.is_a?(String) }
|
24
|
-
event = NewRelic::Security::Agent::Control::Collector.collect(HTTP_REQUEST, [ob])
|
13
|
+
event = NewRelic::Security::Agent::Control::Collector.collect(HTTP_REQUEST, [uri.to_s])
|
25
14
|
NewRelic::Security::Instrumentation::InstrumentationUtils.append_tracing_data(headers, event) if event
|
26
15
|
event
|
27
16
|
rescue => exception
|