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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pr_ci.yml +4 -4
  3. data/.github/workflows/release.yml +1 -1
  4. data/.github/workflows/rubocop.yml +1 -1
  5. data/CHANGELOG.md +90 -1
  6. data/Gemfile_test +3 -0
  7. data/README.md +1 -0
  8. data/THIRD_PARTY_NOTICES.md +8 -0
  9. data/lib/newrelic_security/agent/agent.rb +22 -4
  10. data/lib/newrelic_security/agent/configuration/manager.rb +65 -7
  11. data/lib/newrelic_security/agent/control/application_runtime_error.rb +1 -1
  12. data/lib/newrelic_security/agent/control/collector.rb +41 -4
  13. data/lib/newrelic_security/agent/control/control_command.rb +2 -3
  14. data/lib/newrelic_security/agent/control/error_reporting.rb +8 -6
  15. data/lib/newrelic_security/agent/control/event.rb +15 -1
  16. data/lib/newrelic_security/agent/control/event_processor.rb +25 -14
  17. data/lib/newrelic_security/agent/control/event_subscriber.rb +6 -8
  18. data/lib/newrelic_security/agent/control/health_check.rb +4 -0
  19. data/lib/newrelic_security/agent/control/http_context.rb +10 -6
  20. data/lib/newrelic_security/agent/control/iast_client.rb +24 -11
  21. data/lib/newrelic_security/agent/control/reflected_xss.rb +3 -4
  22. data/lib/newrelic_security/agent/control/scan_scheduler.rb +77 -0
  23. data/lib/newrelic_security/agent/control/websocket_client.rb +71 -16
  24. data/lib/newrelic_security/agent/utils/agent_utils.rb +25 -17
  25. data/lib/newrelic_security/constants.rb +1 -2
  26. data/lib/newrelic_security/instrumentation-security/async-http/instrumentation.rb +2 -13
  27. data/lib/newrelic_security/instrumentation-security/curb/instrumentation.rb +1 -14
  28. data/lib/newrelic_security/instrumentation-security/ethon/chain.rb +0 -6
  29. data/lib/newrelic_security/instrumentation-security/ethon/instrumentation.rb +7 -42
  30. data/lib/newrelic_security/instrumentation-security/ethon/prepend.rb +0 -4
  31. data/lib/newrelic_security/instrumentation-security/excon/instrumentation.rb +3 -13
  32. data/lib/newrelic_security/instrumentation-security/grape/instrumentation.rb +1 -0
  33. data/lib/newrelic_security/instrumentation-security/graphql/chain.rb +26 -0
  34. data/lib/newrelic_security/instrumentation-security/graphql/instrumentation.rb +28 -0
  35. data/lib/newrelic_security/instrumentation-security/graphql/prepend.rb +18 -0
  36. data/lib/newrelic_security/instrumentation-security/grpc/server/instrumentation.rb +3 -2
  37. data/lib/newrelic_security/instrumentation-security/httpclient/instrumentation.rb +4 -28
  38. data/lib/newrelic_security/instrumentation-security/httprb/instrumentation.rb +1 -12
  39. data/lib/newrelic_security/instrumentation-security/httpx/instrumentation.rb +1 -15
  40. data/lib/newrelic_security/instrumentation-security/instrumentation_utils.rb +0 -17
  41. data/lib/newrelic_security/instrumentation-security/io/chain.rb +2 -2
  42. data/lib/newrelic_security/instrumentation-security/io/prepend.rb +1 -1
  43. data/lib/newrelic_security/instrumentation-security/net_http/instrumentation.rb +6 -23
  44. data/lib/newrelic_security/instrumentation-security/net_ldap/instrumentation.rb +1 -1
  45. data/lib/newrelic_security/instrumentation-security/padrino/instrumentation.rb +1 -0
  46. data/lib/newrelic_security/instrumentation-security/patron/instrumentation.rb +2 -15
  47. data/lib/newrelic_security/instrumentation-security/rack/chain.rb +24 -0
  48. data/lib/newrelic_security/instrumentation-security/rack/instrumentation.rb +44 -0
  49. data/lib/newrelic_security/instrumentation-security/rack/prepend.rb +18 -0
  50. data/lib/newrelic_security/instrumentation-security/rails/instrumentation.rb +1 -0
  51. data/lib/newrelic_security/instrumentation-security/roda/instrumentation.rb +1 -0
  52. data/lib/newrelic_security/instrumentation-security/sinatra/instrumentation.rb +1 -0
  53. data/lib/newrelic_security/newrelic-security-api/api.rb +1 -1
  54. data/lib/newrelic_security/parse-cron/cron_parser.rb +294 -0
  55. data/lib/newrelic_security/version.rb +1 -1
  56. data/lib/newrelic_security/websocket-client-simple/client.rb +5 -1
  57. data/newrelic_security.gemspec +1 -1
  58. metadata +15 -7
@@ -9,7 +9,7 @@ module NewRelic::Security
9
9
 
10
10
  class EventProcessor
11
11
 
12
- attr_accessor :eventQ, :event_dequeue_thread, :healthcheck_thread
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
- NewRelic::Security::Agent.logger.info "Sending application info : #{app_info.to_json}"
28
- NewRelic::Security::Agent.init_logger.info "Sending application info : #{app_info.to_json}"
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
- NewRelic::Security::Agent.logger.info "Sending application URL Mappings : #{application_url_mappings.to_json}"
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
- # TODO: Create 3 or more consumers for event sending
90
- @event_dequeue_thread = Thread.new do
91
- Thread.current.name = "newrelic_security_event_thread"
92
- loop do
93
- begin
94
- data_to_be_sent = @eventQ.pop
95
- NewRelic::Security::Agent::Control::WebsocketClient.instance.send(data_to_be_sent)
96
- rescue => exception
97
- NewRelic::Security::Agent.logger.error "Exception in event pop operation : #{exception.inspect}"
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.init
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(NewRelic::Security::Agent.config[:'security.request.body_limit'] * 1024) #after read, offset changes
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(NewRelic::Security::Agent.config[:'security.request.body_limit'] * 1024)
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(NewRelic::Security::Agent.config[:'security.request.body_limit'] * 1024)
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(NewRelic::Security::Agent.config[:'security.request.body_limit'] * 1024)
46
+ @body = strio.read(REQUEST_BODY_LIMIT * 1024)
44
47
  end
45
- @data_truncated = @body && @body.size >= NewRelic::Security::Agent.config[:'security.request.body_limit'] * 1024
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
- sleep NewRelic::Security::Agent.config[:policy][VULNERABILITY_SCAN][IAST_SCAN][PROBING][INTERVAL]
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.send_iast_data_transfer_request(iast_data_transfer_request)
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 = service.rpc_stub_class.new("localhost:#{request[SERVER_PORT_1]}", :this_channel_is_insecure) unless @stub
138
+ @stub ||= service.rpc_stub_class.new("localhost:#{request[SERVER_PORT_1]}", :this_channel_is_insecure)
126
139
 
127
- parsed_body = request[BODY][1..-2].split(',')
128
- if reflected_metadata[IS_GRPC_CLIENT_STREAM]
129
- chunks_enum = Enumerator.new do |y|
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
- else
135
- chunks_enum = Object.const_get(reflected_metadata[INPUT_CLASS]).decode_json(request[BODY])
136
- end
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
- # Unescaping of xml data is remaining
112
- processed_data.add(body)
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
- Thread.new { NewRelic::Security::Agent.agent.reconnect(0) } if e
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(true) }
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 = message.to_json
107
- NewRelic::Security::Agent.logger.debug "Sending #{message.jsonName} : #{message_json}"
108
- res = @ws.send(message_json)
109
- if res && message.jsonName == :Event
110
- NewRelic::Security::Agent.agent.event_sent_count.increment
111
- if NewRelic::Security::Agent::Utils.is_IAST_request?(message.httpRequest[:headers])
112
- NewRelic::Security::Agent.agent.iast_event_stats.sent.increment
113
- else
114
- NewRelic::Security::Agent.agent.rasp_event_stats.sent.increment
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 false if NewRelic::Security::Agent.config[:policy].empty?
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
- if framework == :rails
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
- elsif framework == :sinatra
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
- elsif framework == :grape
117
- ObjectSpace.each_object(::Grape::Endpoint) { |z|
118
- z.instance_variable_get(:@routes)&.each { |route|
119
- http_method = route.instance_variable_get(:@request_method) || route.instance_variable_get(:@options)[:method]
120
- NewRelic::Security::Agent.agent.route_map << "#{http_method}@#{route.pattern.origin}"
121
- }
122
- }
123
- elsif framework == :padrino
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
- elsif framework == :roda
130
- NewRelic::Security::Agent.logger.warn "TODO: Roda is a routing tree web toolkit, which generates route dynamically, hence route extraction is not possible."
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(method, url, headers, body)
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
- ob[:scheme] = uri.scheme
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