newrelic_security 0.2.0 → 0.4.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/.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
|