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.
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