newrelic_security 0.1.0 → 0.3.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pr_ci.yml +2 -2
  3. data/CHANGELOG.md +82 -0
  4. data/THIRD_PARTY_NOTICES.md +8 -0
  5. data/lib/newrelic_security/agent/agent.rb +24 -4
  6. data/lib/newrelic_security/agent/configuration/manager.rb +51 -8
  7. data/lib/newrelic_security/agent/control/app_info.rb +2 -0
  8. data/lib/newrelic_security/agent/control/application_runtime_error.rb +95 -0
  9. data/lib/newrelic_security/agent/control/application_url_mappings.rb +2 -0
  10. data/lib/newrelic_security/agent/control/collector.rb +34 -3
  11. data/lib/newrelic_security/agent/control/control_command.rb +2 -3
  12. data/lib/newrelic_security/agent/control/critical_message.rb +2 -0
  13. data/lib/newrelic_security/agent/control/error_reporting.rb +74 -0
  14. data/lib/newrelic_security/agent/control/event.rb +26 -4
  15. data/lib/newrelic_security/agent/control/event_processor.rb +26 -0
  16. data/lib/newrelic_security/agent/control/event_subscriber.rb +2 -8
  17. data/lib/newrelic_security/agent/control/exit_event.rb +2 -0
  18. data/lib/newrelic_security/agent/control/grpc_context.rb +2 -1
  19. data/lib/newrelic_security/agent/control/health_check.rb +5 -0
  20. data/lib/newrelic_security/agent/control/http_context.rb +16 -7
  21. data/lib/newrelic_security/agent/control/iast_client.rb +24 -11
  22. data/lib/newrelic_security/agent/control/iast_data_transfer_request.rb +2 -0
  23. data/lib/newrelic_security/agent/control/scan_scheduler.rb +77 -0
  24. data/lib/newrelic_security/agent/control/websocket_client.rb +23 -0
  25. data/lib/newrelic_security/agent/utils/agent_utils.rb +14 -9
  26. data/lib/newrelic_security/constants.rb +2 -2
  27. data/lib/newrelic_security/instrumentation-security/async-http/instrumentation.rb +2 -13
  28. data/lib/newrelic_security/instrumentation-security/curb/instrumentation.rb +1 -14
  29. data/lib/newrelic_security/instrumentation-security/ethon/chain.rb +0 -6
  30. data/lib/newrelic_security/instrumentation-security/ethon/instrumentation.rb +7 -42
  31. data/lib/newrelic_security/instrumentation-security/ethon/prepend.rb +0 -4
  32. data/lib/newrelic_security/instrumentation-security/excon/instrumentation.rb +3 -13
  33. data/lib/newrelic_security/instrumentation-security/grape/chain.rb +7 -2
  34. data/lib/newrelic_security/instrumentation-security/grape/instrumentation.rb +3 -1
  35. data/lib/newrelic_security/instrumentation-security/grape/prepend.rb +7 -1
  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/net_http/instrumentation.rb +6 -23
  42. data/lib/newrelic_security/instrumentation-security/net_ldap/instrumentation.rb +1 -1
  43. data/lib/newrelic_security/instrumentation-security/padrino/chain.rb +17 -0
  44. data/lib/newrelic_security/instrumentation-security/padrino/instrumentation.rb +15 -2
  45. data/lib/newrelic_security/instrumentation-security/padrino/prepend.rb +12 -0
  46. data/lib/newrelic_security/instrumentation-security/patron/instrumentation.rb +2 -15
  47. data/lib/newrelic_security/instrumentation-security/rails/chain.rb +26 -2
  48. data/lib/newrelic_security/instrumentation-security/rails/instrumentation.rb +29 -3
  49. data/lib/newrelic_security/instrumentation-security/rails/prepend.rb +18 -0
  50. data/lib/newrelic_security/instrumentation-security/roda/chain.rb +7 -2
  51. data/lib/newrelic_security/instrumentation-security/roda/instrumentation.rb +3 -1
  52. data/lib/newrelic_security/instrumentation-security/roda/prepend.rb +7 -1
  53. data/lib/newrelic_security/instrumentation-security/sinatra/chain.rb +6 -0
  54. data/lib/newrelic_security/instrumentation-security/sinatra/instrumentation.rb +9 -0
  55. data/lib/newrelic_security/instrumentation-security/sinatra/prepend.rb +4 -0
  56. data/lib/newrelic_security/instrumentation-security/sqlite3/instrumentation.rb +4 -4
  57. data/lib/newrelic_security/newrelic-security-api/api.rb +1 -1
  58. data/lib/newrelic_security/parse-cron/cron_parser.rb +294 -0
  59. data/lib/newrelic_security/version.rb +1 -1
  60. data/newrelic_security.gemspec +1 -1
  61. metadata +8 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c07c9ef96a8884449bdd0d5be44eccf3fee5ae6dde565863fe1eb24449a902f6
4
- data.tar.gz: 56f813a8034eed9fd717a0c549275cd1c1e1386c50acdb2142379fbcc980838d
3
+ metadata.gz: 82cddb6a77c7bac2c46e31b5e0577dd346aa0e78b919fc8fb3c2765a7192c0a6
4
+ data.tar.gz: cb8f47120816909cb30ae0d16f9c6c4da3f684a8497747ccab3bbf12aa36e501
5
5
  SHA512:
6
- metadata.gz: 0a3b7c2e86f9ea2c3a6c4c28da88676f904e78c5cce82fb152897aade9acda32ca9ea485d26dfa7ebb6ef1356c14df9d85407971d05a0a0f69e1ec43824227a1
7
- data.tar.gz: d3c6dcd24fe5ed0b54537b503e6994e6ed6701d5bab53f90220cce4b300e14a619db19cab4befec08560b72c941898a082a1b3c9738ed29e3fb8feae714edeb2
6
+ metadata.gz: 96abdbedd151b564c30d2ccafc1e98b46185130d6028634877ef0a42c1dc8ff775cf5affbcb7fdd7eee305bbb5ca33a266dfd82ebd5b7f0d2f6e396687af7897
7
+ data.tar.gz: a69f0fcaf8a427a4ec169ed7dd33952ee88e723a89281a2e9b46841029898fdea6a21748290e2058e861629e368da84b0b6d317181248b33f88a3a33bc5049e2
@@ -72,6 +72,6 @@ jobs:
72
72
  with:
73
73
  token: ${{ secrets.GITHUB_TOKEN }}
74
74
  resultPath: lib/coverage_results/.last_run.json
75
- failedThreshold: 70
76
- failedThresholdBranch: 33
75
+ failedThreshold: 67
76
+ failedThresholdBranch: 25
77
77
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # New Relic Ruby Security Agent Release Notes
2
2
 
3
+ ## v0.3.0
4
+
5
+ Version 0.3.0 introduces more control on IAST scanning through new configs(exclude_from_iast_scan, scan_schedule & scan_controllers) and
6
+ features like API inventory for gRPC server and IAST scan start related timestamps.
7
+
8
+ Updated json_version: **1.2.8**
9
+
10
+ - Feature: IAST scan exclusion for apis, http request parameters(header, query & body) & IAST detection categories and scan scheduling through delay, duration & cron schedule. [PR#131](https://github.com/newrelic/csec-ruby-agent/pull/131)
11
+
12
+ - Feature: IAST scan request rate limit to control IAST scan request firing. [PR#132](https://github.com/newrelic/csec-ruby-agent/pull/132)
13
+
14
+ - Feature: API endpoints support for gRPC server applications. [PR#143](https://github.com/newrelic/csec-ruby-agent/pull/143)
15
+
16
+ - Feature: Reporting of IAST scanning application procStartTime, trafficStartedTime & scanStartTime. [PR#136](https://github.com/newrelic/csec-ruby-agent/pull/136)
17
+
18
+ - Misc Chore: Optimised SSRF events parameters to send only URL in parameters. [PR#129](https://github.com/newrelic/csec-ruby-agent/pull/129)
19
+
20
+ ##### New security configs
21
+
22
+ ```yaml
23
+ security:
24
+ exclude_from_iast_scan:
25
+ api: []
26
+ http_request_parameters:
27
+ header: []
28
+ query: []
29
+ body: []
30
+ iast_detection_category:
31
+ insecure_settings: false
32
+ invalid_file_access: false
33
+ sql_injection: false
34
+ nosql_injection: false
35
+ ldap_injection: false
36
+ javascript_injection: false
37
+ command_injection: false
38
+ xpath_injection: false
39
+ ssrf: false
40
+ rxss: false
41
+ scan_schedule:
42
+ delay: 0
43
+ duration: 0
44
+ schedule: ""
45
+ always_sample_traces: false
46
+ scan_controllers:
47
+ iast_scan_request_rate_limit: 3600
48
+ ```
49
+
50
+ ##### Deprecated security configs (will be removed in next major release v1.0.0)
51
+ ```yaml
52
+ security:
53
+ request:
54
+ body_limit: 300
55
+ detection:
56
+ rci:
57
+ enabled: true
58
+ rxss:
59
+ enabled: true
60
+ deserialization:
61
+ enabled: true
62
+ ```
63
+
64
+ ## v0.2.0
65
+
66
+ Version 0.2.0 introuduces Error reporting as part of security. Any unhandled or 5xx errors in application runtime will now be visible in IAST capability UI. Updated json_version: **1.2.4**
67
+
68
+ - Feature: Unhandled and 5xx error reporting [PR#134](https://github.com/newrelic/csec-ruby-agent/pull/134)
69
+
70
+ - Bugfix: Fix for API route not present in rails7 [PR#127](https://github.com/newrelic/csec-ruby-agent/pull/127)
71
+
72
+ - Bugfix: Fix for Sqlite3 parameters sent in wrong fromat [PR#130](https://github.com/newrelic/csec-ruby-agent/pull/130)
73
+
74
+ - Bugfix: Fix for multiple events have same id [PR#135](https://github.com/newrelic/csec-ruby-agent/pull/135)
75
+
76
+ - Bugfix: Fix for NR_CSEC_VALIDATOR_HOME_TMP placeholder value not replaced during File Access fuzzing [PR#138](https://github.com/newrelic/csec-ruby-agent/pull/138)
77
+
78
+ - Bugfix: Fix for appServerInfo fields are not present in File Operation events [PR#139](https://github.com/newrelic/csec-ruby-agent/pull/139)
79
+
80
+ - Sending security agent critical errors to APM error inbox [PR#137](https://github.com/newrelic/csec-ruby-agent/pull/137)
81
+
82
+ - Added key identifiers in entityGuid and acccountId in all json reporting [PR#101](https://github.com/newrelic/csec-ruby-agent/pull/101)
83
+
84
+
3
85
  ## v0.1.0
4
86
 
5
87
  Version 0.1.0 introduces `newrelic_security` agent for public preview under Newrelic pre-release software notice.
@@ -34,3 +34,11 @@ Distributed under the following license(s):
34
34
 
35
35
  * [The MIT License](http://opensource.org/licenses/MIT)
36
36
 
37
+
38
+ ## [parse-cron](https://github.com/siebertm/parse-cron)
39
+
40
+ Copyright (C) 2013 Michael Siebert
41
+
42
+ Distributed under the following license(s):
43
+
44
+ * [The MIT License](http://opensource.org/licenses/MIT)
@@ -17,13 +17,16 @@ require 'newrelic_security/agent/control/critical_message'
17
17
  require 'newrelic_security/agent/control/event_counter'
18
18
  require 'newrelic_security/agent/control/event_stats'
19
19
  require 'newrelic_security/agent/control/exit_event'
20
+ require 'newrelic_security/agent/control/application_runtime_error'
21
+ require 'newrelic_security/agent/control/error_reporting'
22
+ require 'newrelic_security/agent/control/scan_scheduler'
20
23
  require 'newrelic_security/instrumentation-security/instrumentation_loader'
21
24
 
22
25
  module NewRelic::Security
23
26
  module Agent
24
27
  class Agent
25
28
 
26
- attr_accessor :websocket_client, :event_processor, :iast_client, :http_request_count, :event_processed_count, :event_sent_count, :event_drop_count, :route_map, :iast_event_stats, :rasp_event_stats, :exit_event_stats
29
+ attr_accessor :websocket_client, :event_processor, :iast_client, :http_request_count, :event_processed_count, :event_sent_count, :event_drop_count, :route_map, :iast_event_stats, :rasp_event_stats, :exit_event_stats, :error_reporting, :scan_scheduler
27
30
 
28
31
  def initialize
29
32
  NewRelic::Security::Agent.config
@@ -41,6 +44,8 @@ module NewRelic::Security
41
44
  @iast_event_stats = NewRelic::Security::Agent::Control::EventStats.new
42
45
  @rasp_event_stats = NewRelic::Security::Agent::Control::EventStats.new
43
46
  @exit_event_stats = NewRelic::Security::Agent::Control::EventStats.new
47
+ @error_reporting = NewRelic::Security::Agent::Control::ErrorReporting.new
48
+ @scan_scheduler = NewRelic::Security::Agent::Control::ScanScheduler.new
44
49
  end
45
50
 
46
51
  def init
@@ -56,11 +61,24 @@ module NewRelic::Security
56
61
  NewRelic::Security::Agent.logger.error "Exception in security agent init: #{exception.inspect} #{exception.backtrace}\n"
57
62
  end
58
63
 
64
+ def shutdown_security_agent
65
+ @iast_client&.fuzzQ&.clear
66
+ @iast_client&.completed_requests&.clear
67
+ @iast_client&.pending_request_ids&.clear
68
+ @iast_client&.iast_data_transfer_request_processor_thread&.kill
69
+ NewRelic::Security::Agent.config.disable_security
70
+ stop_websocket_client_if_open
71
+ end
72
+
59
73
  def start_websocket_client
60
- NewRelic::Security::Agent::Control::WebsocketClient.instance.close(false) if NewRelic::Security::Agent::Control::WebsocketClient.instance.is_open?
74
+ stop_websocket_client_if_open
61
75
  @websocket_client = NewRelic::Security::Agent::Control::WebsocketClient.instance.connect
62
76
  end
63
77
 
78
+ def stop_websocket_client_if_open
79
+ NewRelic::Security::Agent::Control::WebsocketClient.instance.close(false) if NewRelic::Security::Agent::Control::WebsocketClient.instance.is_open?
80
+ end
81
+
64
82
  def start_event_processor
65
83
  @event_processor&.event_dequeue_thread&.kill
66
84
  @event_processor&.healthcheck_thread&.kill
@@ -69,9 +87,10 @@ module NewRelic::Security
69
87
  end
70
88
 
71
89
  def start_iast_client
72
- @iast_client&.iast_dequeue_threads&.each { |t| t.kill if t }
90
+ @iast_client&.iast_dequeue_threads&.each { |t| t&.kill }
73
91
  @iast_client&.iast_data_transfer_request_processor_thread&.kill
74
92
  @iast_client = nil
93
+ NewRelic::Security::Agent.logger.info "Starting IAST client now at current time: #{Time.now}"
75
94
  @iast_client = NewRelic::Security::Agent::Control::IASTClient.new
76
95
  end
77
96
 
@@ -93,7 +112,8 @@ module NewRelic::Security
93
112
  def find_or_create_file_path(path)
94
113
  ::FileUtils.mkdir_p(path) unless ::File.directory?(path)
95
114
  ::File.directory?(path)
96
- rescue
115
+ rescue => e
116
+ ::NewRelic::Agent.notice_error(e)
97
117
  return false
98
118
  end
99
119
 
@@ -27,19 +27,42 @@ module NewRelic::Security
27
27
  @cache[:log_level] = ::NewRelic::Agent.config[:log_level]
28
28
  @cache[:high_security] = ::NewRelic::Agent.config[:high_security]
29
29
  @cache[:'agent.enabled'] = ::NewRelic::Agent.config[:'security.agent.enabled']
30
- @cache[:enabled] = ::NewRelic::Agent.config[:'security.enabled']
30
+ @cache[:'security.enabled'] = ::NewRelic::Agent.config[:'security.enabled']
31
+ @cache[:enabled] = false
31
32
  @cache[:mode] = ::NewRelic::Agent.config[:'security.mode']
32
33
  @cache[:validator_service_url] = ::NewRelic::Agent.config[:'security.validator_service_url']
33
- @cache[:'security.detection.rci.enabled'] = ::NewRelic::Agent.config[:'security.detection.rci.enabled']
34
- @cache[:'security.detection.rxss.enabled'] = ::NewRelic::Agent.config[:'security.detection.rxss.enabled']
35
- @cache[:'security.detection.deserialization.enabled'] = ::NewRelic::Agent.config[:'security.detection.deserialization.enabled']
34
+ # TODO: Remove security.detection.* & security.request.body_limit in next major release
35
+ @cache[:'security.detection.rci.enabled'] = ::NewRelic::Agent.config[:'security.detection.rci.enabled'].nil? ? true : ::NewRelic::Agent.config[:'security.detection.rci.enabled']
36
+ @cache[:'security.detection.rxss.enabled'] = ::NewRelic::Agent.config[:'security.detection.rxss.enabled'].nil? ? true : ::NewRelic::Agent.config[:'security.detection.rxss.enabled']
37
+ @cache[:'security.detection.deserialization.enabled'] = ::NewRelic::Agent.config[:'security.detection.deserialization.enabled'].nil? ? true : ::NewRelic::Agent.config[:'security.detection.deserialization.enabled']
38
+ @cache[:'security.scan_controllers.iast_scan_request_rate_limit'] = ::NewRelic::Agent.config[:'security.scan_controllers.iast_scan_request_rate_limit'].to_i
36
39
  @cache[:framework] = detect_framework
37
40
  @cache[:'security.application_info.port'] = ::NewRelic::Agent.config[:'security.application_info.port'].to_i
38
- @cache[:'security.request.body_limit'] = ::NewRelic::Agent.config[:'security.request.body_limit'].to_i > 0 ? ::NewRelic::Agent.config[:'security.request.body_limit'].to_i : 300
39
41
  @cache[:listen_port] = nil
42
+ @cache[:process_start_time] = current_time_millis # TODO: Ruby doesn't provide process start time in pure ruby implementation using agent loading time for now.
43
+ @cache[:traffic_start_time] = nil
44
+ @cache[:scan_start_time] = nil
40
45
  @cache[:app_root] = NewRelic::Security::Agent::Utils.app_root
41
46
  @cache[:jruby_objectspace_enabled] = false
42
- @cache[:json_version] = :'1.2.0'
47
+ @cache[:json_version] = :'1.2.8'
48
+ @cache[:'security.exclude_from_iast_scan.api'] = convert_to_regexp_list(::NewRelic::Agent.config[:'security.exclude_from_iast_scan.api'])
49
+ @cache[:'security.exclude_from_iast_scan.http_request_parameters.header'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.header']
50
+ @cache[:'security.exclude_from_iast_scan.http_request_parameters.query'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.query']
51
+ @cache[:'security.exclude_from_iast_scan.http_request_parameters.body'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.body']
52
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.insecure_settings'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.insecure_settings']
53
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.invalid_file_access'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.invalid_file_access']
54
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.sql_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.sql_injection']
55
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.nosql_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.nosql_injection']
56
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.ldap_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ldap_injection']
57
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.javascript_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.javascript_injection']
58
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.command_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.command_injection']
59
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.xpath_injection'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.xpath_injection']
60
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.ssrf'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ssrf']
61
+ @cache[:'security.exclude_from_iast_scan.iast_detection_category.rxss'] = ::NewRelic::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.rxss']
62
+ @cache[:'security.scan_schedule.delay'] = ::NewRelic::Agent.config[:'security.scan_schedule.delay'].to_i
63
+ @cache[:'security.scan_schedule.duration'] = ::NewRelic::Agent.config[:'security.scan_schedule.duration'].to_i
64
+ @cache[:'security.scan_schedule.schedule'] = ::NewRelic::Agent.config[:'security.scan_schedule.schedule']
65
+ @cache[:'security.scan_schedule.always_sample_traces'] = ::NewRelic::Agent.config[:'security.scan_schedule.always_sample_traces']
43
66
 
44
67
  @environment_source = NewRelic::Security::Agent::Configuration::EnvironmentSource.new
45
68
  @server_source = NewRelic::Security::Agent::Configuration::ServerSource.new
@@ -47,8 +70,7 @@ module NewRelic::Security
47
70
  @yaml_source = NewRelic::Security::Agent::Configuration::YamlSource.new
48
71
  @default_source = NewRelic::Security::Agent::Configuration::DefaultSource.new
49
72
  rescue Exception => exception
50
- # TODO: remove this puts once agent stablizes
51
- puts "Exception in Configuration::Manager.initialize : #{exception.inspect} #{exception.backtrace}"
73
+ ::NewRelic::Agent.notice_error(exception)
52
74
  end
53
75
 
54
76
  def [](key)
@@ -94,6 +116,14 @@ module NewRelic::Security
94
116
  @cache[:listen_port] = listen_port
95
117
  end
96
118
 
119
+ def traffic_start_time=(traffic_start_time)
120
+ @cache[:traffic_start_time] = traffic_start_time
121
+ end
122
+
123
+ def scan_start_time=(scan_start_time)
124
+ @cache[:scan_start_time] = scan_start_time
125
+ end
126
+
97
127
  def app_server=(app_server)
98
128
  @cache[:app_server] = app_server
99
129
  end
@@ -172,6 +202,19 @@ module NewRelic::Security
172
202
  def generate_key(entity_guid)
173
203
  ::OpenSSL::PKCS5.pbkdf2_hmac(entity_guid, entity_guid[0..15], 1024, 32, SHA1)
174
204
  end
205
+
206
+ def current_time_millis
207
+ (Time.now.to_f * 1000).to_i
208
+ end
209
+
210
+ def convert_to_regexp_list(value_list)
211
+ value_list.map do |value|
212
+ next unless value && !value.empty?
213
+ value = "^#{value}" if value[0] != '^'
214
+ value = "#{value}$" if value[-1] != '$'
215
+ /#{value}/
216
+ end
217
+ end
175
218
  end
176
219
  end
177
220
  end
@@ -26,6 +26,8 @@ module NewRelic::Security
26
26
  @jsonVersion = NewRelic::Security::Agent.config[:json_version]
27
27
  @startTime = current_time_millis
28
28
  @applicationUUID = NewRelic::Security::Agent.config[:uuid]
29
+ @appAccountId = NewRelic::Security::Agent.config[:account_id]
30
+ @appEntityGuid = NewRelic::Security::Agent.config[:entity_guid]
29
31
  @framework = NewRelic::Security::Agent.config[:framework]
30
32
  @groupName = NewRelic::Security::Agent.config[:mode]
31
33
  @userProvidedApplicationInfo = Hash.new
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest'
5
+
6
+ module NewRelic::Security
7
+ module Agent
8
+ module Control
9
+ class ApplicationRuntimeError
10
+ attr_reader :jsonName, :exception
11
+ attr_accessor :counter
12
+
13
+ def initialize(exception, ctxt, response_code, category)
14
+ @collectorType = RUBY
15
+ @language = Ruby
16
+ @jsonName = :'application-runtime-error'
17
+ @eventType = :'application-runtime-error'
18
+ @collectorVersion = NewRelic::Security::VERSION
19
+ @buildNumber = nil
20
+ @jsonVersion = NewRelic::Security::Agent.config[:json_version]
21
+ @timestamp = current_time_millis
22
+ @applicationUUID = NewRelic::Security::Agent.config[:uuid]
23
+ @appAccountId = NewRelic::Security::Agent.config[:account_id]
24
+ @appEntityGuid = NewRelic::Security::Agent.config[:entity_guid]
25
+ @framework = NewRelic::Security::Agent.config[:framework]
26
+ @groupName = NewRelic::Security::Agent.config[:mode]
27
+ @policyVersion = nil
28
+ @linkingMetadata = add_linking_metadata
29
+ @httpRequest = get_http_request_data(ctxt)
30
+ @exception = exception
31
+ @counter = 1
32
+ @responseCode = response_code
33
+ @category = category
34
+ @traceId = generate_trace_id(ctxt, category)
35
+ end
36
+
37
+ def as_json
38
+ instance_variables.map! do |ivar|
39
+ [ivar[1..-1].to_sym, instance_variable_get(ivar)]
40
+ end.to_h
41
+ end
42
+
43
+ def to_json(*_args)
44
+ as_json.to_json
45
+ end
46
+
47
+ private
48
+
49
+ def current_time_millis
50
+ (Time.now.to_f * 1000).to_i
51
+ end
52
+
53
+ def add_linking_metadata
54
+ linking_metadata = {}
55
+ linking_metadata[:agentRunId] = NewRelic::Security::Agent.config[:agent_run_id]
56
+ linking_metadata.merge!(NewRelic::Security::Agent.config[:linking_metadata])
57
+ # TODO: add other fields as well in linking metadata, for event and heathcheck as well
58
+ end
59
+
60
+ def get_http_request_data(ctxt)
61
+ return if ctxt.nil?
62
+ http_request = {}
63
+ http_request[:parameterMap] = {}
64
+ http_request[:body] = ctxt.body
65
+ http_request[:generationTime] = ctxt.time_stamp
66
+ http_request[:dataTruncated] = false
67
+ http_request[:method] = ctxt.method
68
+ http_request[:route] = ctxt.route.split(AT_THE_RATE)[1] if ctxt.route
69
+ http_request[:url] = URI(ctxt.req[REQUEST_URI]).respond_to?(:request_uri) ? URI(ctxt.req[REQUEST_URI]).request_uri : ctxt.req[REQUEST_URI]
70
+ http_request[:requestURI] = "#{ctxt.req[RACK_URL_SCHEME]}://#{ctxt.req[HTTP_HOST]}#{ctxt.req[PATH_INFO]}"
71
+ http_request[:clientIP] = ctxt.headers.key?(X_FORWARDED_FOR) ? ctxt.headers[X_FORWARDED_FOR].split(COMMA)[0].to_s : ctxt.req[REMOTE_ADDR].to_s
72
+ http_request[:serverPort] = ctxt.req[SERVER_PORT].to_i
73
+ http_request[:protocol] = ctxt.req[RACK_URL_SCHEME]
74
+ http_request[:contextPath] = ROOT_PATH
75
+ http_request[:headers] = ctxt.headers
76
+ http_request[:contentType] = ctxt.req[CONTENT_TYPE] if ctxt.req.key?(CONTENT_TYPE)
77
+ http_request[:headers][CONTENT_TYPE1] = ctxt.req[CONTENT_TYPE] if ctxt.req.key?(CONTENT_TYPE)
78
+ http_request[:dataTruncated] = ctxt.data_truncated
79
+ http_request
80
+ end
81
+
82
+ def generate_trace_id(ctxt, category)
83
+ @exception[:stackTrace]
84
+ method, route = ctxt.route.split(AT_THE_RATE) if ctxt.route
85
+ if @exception[:stackTrace]
86
+ ::Digest::SHA256.hexdigest("#{@exception[:stackTrace].join(PIPE)}#{category}#{route}#{method}").to_s
87
+ else
88
+ ::Digest::SHA256.hexdigest("#{category}#{route}#{method}").to_s
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+ end
@@ -21,6 +21,8 @@ module NewRelic::Security
21
21
  @jsonVersion = NewRelic::Security::Agent.config[:json_version]
22
22
  @timestamp = current_time_millis
23
23
  @applicationUUID = NewRelic::Security::Agent.config[:uuid]
24
+ @appAccountId = NewRelic::Security::Agent.config[:account_id]
25
+ @appEntityGuid = NewRelic::Security::Agent.config[:entity_guid]
24
26
  @framework = NewRelic::Security::Agent.config[:framework]
25
27
  @groupName = NewRelic::Security::Agent.config[:mode]
26
28
  @policyVersion = nil
@@ -19,6 +19,8 @@ module NewRelic::Security
19
19
  def collect(case_type, args, event_category = nil, **keyword_args)
20
20
  return unless NewRelic::Security::Agent.config[:enabled]
21
21
  return if NewRelic::Security::Agent::Control::HTTPContext.get_context.nil? && NewRelic::Security::Agent::Control::GRPCContext.get_context.nil?
22
+ return if check_and_exclude_from_iast_scan_for_api
23
+ return if check_and_exclude_from_iast_scan_for_detection_category(case_type)
22
24
  args.map! { |file| Pathname.new(file).relative? ? File.join(Dir.pwd, file) : file } if [FILE_OPERATION, FILE_INTEGRITY].include?(case_type)
23
25
 
24
26
  event = NewRelic::Security::Agent::Control::Event.new(case_type, args, event_category)
@@ -44,7 +46,7 @@ module NewRelic::Security
44
46
  # Hence, considering only frame absolute_path & lineno for apiId calculation.
45
47
  user_frame_index = get_user_frame_index(stk)
46
48
  event.apiId = "#{case_type}-#{calculate_api_id(stk[0..user_frame_index].map { |frame| "#{frame.absolute_path}:#{frame.lineno}" }, event.httpRequest[:method], route)}"
47
- stk.delete_if {|frame| frame.path.match(/newrelic_security/) || frame.path.match(/new_relic/)}
49
+ stk.delete_if { |frame| frame.path.match?(/newrelic_security/) || frame.path.match?(/new_relic/) }
48
50
  user_frame_index = get_user_frame_index(stk)
49
51
  return if case_type != REFLECTED_XSS && user_frame_index == -1 # TODO: Add log message here: "Filtered because User Stk frame NOT FOUND \r\n"
50
52
  if user_frame_index != -1
@@ -59,8 +61,8 @@ module NewRelic::Security
59
61
  end
60
62
  event.stacktrace = stk[0..user_frame_index].map(&:to_s)
61
63
  NewRelic::Security::Agent.agent.event_processor.send_event(event)
62
- if event.httpRequest[:headers].key?(NR_CSEC_FUZZ_REQUEST_ID) && event.apiId == event.httpRequest[:headers][NR_CSEC_FUZZ_REQUEST_ID].split(COLON_IAST_COLON)[0]
63
- NewRelic::Security::Agent.agent.iast_client.completed_requests[event.parentId] << event.id if NewRelic::Security::Agent.agent.iast_client.completed_requests[event.parentId]
64
+ if event.httpRequest[:headers].key?(NR_CSEC_FUZZ_REQUEST_ID) && event.apiId == event.httpRequest[:headers][NR_CSEC_FUZZ_REQUEST_ID].split(COLON_IAST_COLON)[0] && NewRelic::Security::Agent.agent.iast_client.completed_requests[event.parentId]
65
+ NewRelic::Security::Agent.agent.iast_client.completed_requests[event.parentId] << event.id
64
66
  end
65
67
  event
66
68
  rescue Exception => exception
@@ -111,6 +113,35 @@ module NewRelic::Security
111
113
  }
112
114
  end
113
115
 
116
+ def check_and_exclude_from_iast_scan_for_api
117
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.api'].each do |api|
118
+ return true if api&.match?(NewRelic::Security::Agent::Control::HTTPContext.get_context.url)
119
+ end
120
+ return false
121
+ end
122
+
123
+ def check_and_exclude_from_iast_scan_for_detection_category(case_type)
124
+ case case_type
125
+ when FILE_OPERATION, FILE_INTEGRITY
126
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.invalid_file_access']
127
+ when SQL_DB_COMMAND
128
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.sql_injection']
129
+ when NOSQL_DB_COMMAND
130
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.nosql_injection']
131
+ when LDAP
132
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ldap_injection']
133
+ when SYSTEM_COMMAND
134
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.command_injection']
135
+ when XPATH
136
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.xpath_injection']
137
+ when HTTP_REQUEST
138
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.ssrf']
139
+ when REFLECTED_XSS
140
+ NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.iast_detection_category.rxss']
141
+ else
142
+ false
143
+ end
144
+ end
114
145
  end
115
146
  end
116
147
  end
@@ -32,7 +32,7 @@ module NewRelic::Security
32
32
  fuzz_request = NewRelic::Security::Agent::Control::FuzzRequest.new(message_object[:id])
33
33
  fuzz_request.request = prepare_fuzz_request(message_object)
34
34
  fuzz_request.case_type = message_object[:arguments][1]
35
- fuzz_request.reflected_metadata = message_object[:reflectedMetaData]
35
+ fuzz_request.reflected_metadata = message_object[:reflectedMetaData]
36
36
  NewRelic::Security::Agent.agent.iast_client.pending_request_ids << message_object[:id]
37
37
  NewRelic::Security::Agent.agent.iast_client.enqueue(fuzz_request)
38
38
  fuzz_request = nil
@@ -49,8 +49,6 @@ module NewRelic::Security
49
49
  message_object[:arguments].each { |processed_id| NewRelic::Security::Agent.agent.iast_client.completed_requests.delete(processed_id) }
50
50
  when 100
51
51
  NewRelic::Security::Agent.logger.debug "Control command : '100', #{message_object.to_json}"
52
- ::NewRelic::Agent.instance.events.notify(:security_policy_received, message_object[:data])
53
- # TODO: Update policy from file here, if enabled.
54
52
  when 101
55
53
 
56
54
  when 102
@@ -104,6 +102,7 @@ module NewRelic::Security
104
102
 
105
103
  def prepare_fuzz_request(message_object)
106
104
  message_object[:arguments][0].gsub!(NR_CSEC_VALIDATOR_HOME_TMP, NewRelic::Security::Agent.config[:fuzz_dir_path])
105
+ message_object[:arguments][0].gsub!(NR_CSEC_VALIDATOR_HOME_TMP_URL_ENCODED, NewRelic::Security::Agent.config[:fuzz_dir_path])
107
106
  message_object[:arguments][0].gsub!(NR_CSEC_VALIDATOR_FILE_SEPARATOR, ::File::SEPARATOR)
108
107
  prepared_fuzz_request = ::JSON.parse(message_object[:arguments][0])
109
108
  prepared_fuzz_request[HEADERS][NR_CSEC_PARENT_ID] = message_object[:id]
@@ -21,6 +21,8 @@ module NewRelic::Security
21
21
  @buildNumber = nil
22
22
  @jsonVersion = NewRelic::Security::Agent.config[:json_version]
23
23
  @applicationUUID = NewRelic::Security::Agent.config[:uuid]
24
+ @appAccountId = NewRelic::Security::Agent.config[:account_id]
25
+ @appEntityGuid = NewRelic::Security::Agent.config[:entity_guid]
24
26
  @linkingMetadata = add_linking_metadata
25
27
  @timestamp = current_time_millis
26
28
  @message = message
@@ -0,0 +1,74 @@
1
+ module NewRelic::Security
2
+ module Agent
3
+ module Control
4
+ class ErrorReporting
5
+
6
+ STATUS_CODES_5XX = {
7
+ 500 => "Internal Server Error",
8
+ 501 => "Not Implemented",
9
+ 502 => "Bad Gateway",
10
+ 503 => "Service Unavailable",
11
+ 504 => "Gateway Timeout",
12
+ 505 => "HTTP Version Not Supported",
13
+ 506 => "Variant Also Negotiates",
14
+ 507 => "Insufficient Storage",
15
+ 508 => "Loop Detected",
16
+ 509 => "Bandwidth Limit Exceeded",
17
+ 510 => "Not Extended",
18
+ 511 => "Network Authentication Required"
19
+ }.freeze
20
+
21
+ attr_accessor :exceptions_map
22
+
23
+ def initialize
24
+ @exceptions_map = {}
25
+ end
26
+
27
+ def generate_unhandled_exception(noticed_error, ctxt, response_code)
28
+ unhandled_exception = {}
29
+ category = nil
30
+ if noticed_error
31
+ unhandled_exception[:message] = noticed_error.message
32
+ unhandled_exception[:cause] = nil
33
+ unhandled_exception[:type] = noticed_error.exception_class_name
34
+ unhandled_exception[:stackTrace] = noticed_error.stack_trace
35
+ category = noticed_error.exception_class_name
36
+ end
37
+ category = STATUS_CODES_5XX[response_code] if response_code
38
+ application_runtime_error = NewRelic::Security::Agent::Control::ApplicationRuntimeError.new(unhandled_exception, ctxt, response_code, category)
39
+ key = if response_code
40
+ # TODO: when do refactoring of ctxt.route, use both route and method to generate key
41
+ ctxt.route&.+ response_code.to_s
42
+ else
43
+ application_runtime_error.exception[:type] + application_runtime_error.exception[:stackTrace][0]
44
+ end
45
+ application_runtime_error.counter = @exceptions_map[key].counter + 1 if @exceptions_map.key?(key)
46
+ @exceptions_map[key] = application_runtime_error
47
+ rescue Exception => exception
48
+ NewRelic::Security::Agent.logger.error "Exception in generating unhandled exception: #{exception.inspect} #{exception.backtrace}\n"
49
+ end
50
+
51
+ def extract_noticed_error(current_transaction, ctxt, response_code)
52
+ # TODO: Below operation is expensive, talk to APM to get optimized way to do this
53
+ current_transaction.exceptions.each do |_, span|
54
+ current_transaction.segments.each do |segment|
55
+ generate_unhandled_exception(segment.noticed_error, ctxt, response_code) if span[:span_id] == segment.guid
56
+ end
57
+ end
58
+ rescue Exception => exception
59
+ NewRelic::Security::Agent.logger.error "Exception in extract_noticed_error: #{exception.inspect} #{exception.backtrace}\n"
60
+ end
61
+
62
+ def report_unhandled_or_5xx_exceptions(current_transaction, ctxt, response_code = nil)
63
+ http_response_code = response_code || current_transaction&.http_response_code
64
+ if current_transaction.exceptions.empty? && http_response_code&.between?(500, 599)
65
+ generate_unhandled_exception(nil, ctxt, response_code)
66
+ else
67
+ extract_noticed_error(current_transaction, ctxt, response_code) unless current_transaction.exceptions.empty?
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -26,9 +26,24 @@ module NewRelic::Security
26
26
  @buildNumber = nil
27
27
  @jsonVersion = NewRelic::Security::Agent.config[:json_version]
28
28
  @applicationUUID = NewRelic::Security::Agent.config[:uuid]
29
+ @appAccountId = NewRelic::Security::Agent.config[:account_id]
30
+ @appEntityGuid = NewRelic::Security::Agent.config[:entity_guid]
29
31
  @httpRequest = Hash.new
30
32
  @httpResponse = Hash.new
31
- @metaData = { :reflectedMetaData => { :listen_port => NewRelic::Security::Agent.config[:listen_port].to_s } }
33
+ @metaData = {
34
+ :reflectedMetaData => {
35
+ :listen_port => NewRelic::Security::Agent.config[:listen_port].to_s
36
+ },
37
+ :appServerInfo => {
38
+ :applicationDirectory => NewRelic::Security::Agent.config[:app_root],
39
+ :serverBaseDirectory => NewRelic::Security::Agent.config[:app_root]
40
+ },
41
+ :skipScanParameters => {
42
+ :header => NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.header'],
43
+ :query => NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.query'],
44
+ :body => NewRelic::Security::Agent.config[:'security.exclude_from_iast_scan.http_request_parameters.body']
45
+ }
46
+ }
32
47
  @linkingMetadata = add_linking_metadata
33
48
  @pid = pid
34
49
  @parameters = args
@@ -113,7 +128,7 @@ module NewRelic::Security
113
128
  http_request[:contentType] = "TODO: "
114
129
  http_request[:isGrpc] = ctxt.is_grpc
115
130
  @httpRequest = http_request
116
- @metaData = ctxt.metadata
131
+ @metaData.merge!(ctxt.metadata)
117
132
  end
118
133
 
119
134
  private
@@ -128,13 +143,20 @@ module NewRelic::Security
128
143
  end
129
144
 
130
145
  def event_id
131
- "#{Process.pid}:#{::Thread.current.object_id}:#{thread_monotonic_ctr}"
146
+ "#{Process.pid}:#{current_transaction&.guid}:#{thread_monotonic_ctr}"
147
+ end
148
+
149
+ def current_transaction
150
+ ::NewRelic::Agent::Tracer.current_transaction if defined?(::NewRelic::Agent::Tracer)
132
151
  end
133
152
 
134
153
  def thread_monotonic_ctr
135
154
  ctxt = NewRelic::Security::Agent::Control::HTTPContext.get_context if NewRelic::Security::Agent::Control::HTTPContext.get_context
136
155
  ctxt = NewRelic::Security::Agent::Control::GRPCContext.get_context if NewRelic::Security::Agent::Control::GRPCContext.get_context
137
- ctxt.event_counter = ctxt.event_counter + 1 if ctxt
156
+ return unless ctxt
157
+ ctxt.mutex.synchronize do
158
+ ctxt.event_counter = ctxt.event_counter + 1
159
+ end
138
160
  end
139
161
 
140
162
  def add_linking_metadata