contrast-agent 3.16.0 → 4.0.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +2 -3
  3. data/lib/contrast/agent/assess/policy/policy_scanner.rb +17 -6
  4. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +3 -2
  5. data/lib/contrast/agent/inventory.rb +15 -0
  6. data/lib/contrast/agent/inventory/dependencies.rb +50 -0
  7. data/lib/contrast/agent/inventory/dependency_analysis.rb +37 -0
  8. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +104 -0
  9. data/lib/contrast/agent/inventory/gemfile_digest_cache.rb +38 -0
  10. data/lib/contrast/agent/middleware.rb +1 -2
  11. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +4 -3
  12. data/lib/contrast/agent/request_handler.rb +1 -1
  13. data/lib/contrast/agent/static_analysis.rb +2 -2
  14. data/lib/contrast/agent/tracepoint_hook.rb +1 -1
  15. data/lib/contrast/agent/version.rb +1 -1
  16. data/lib/contrast/api/decorators.rb +3 -0
  17. data/lib/contrast/api/decorators/address.rb +0 -1
  18. data/lib/contrast/api/decorators/application_update.rb +1 -1
  19. data/lib/contrast/api/decorators/library.rb +53 -0
  20. data/lib/contrast/api/decorators/library_usage_update.rb +30 -0
  21. data/lib/contrast/components/agent.rb +6 -5
  22. data/lib/contrast/components/config.rb +29 -37
  23. data/lib/contrast/components/interface.rb +25 -3
  24. data/lib/contrast/components/inventory.rb +6 -1
  25. data/lib/contrast/config/inventory_configuration.rb +2 -2
  26. data/lib/contrast/framework/rails/support.rb +3 -0
  27. data/lib/contrast/logger/application.rb +1 -1
  28. data/lib/contrast/utils/inventory_util.rb +0 -7
  29. data/lib/contrast/utils/sha256_builder.rb +0 -12
  30. data/service_executables/VERSION +1 -1
  31. data/service_executables/linux/contrast-service +0 -0
  32. data/service_executables/mac/contrast-service +0 -0
  33. metadata +9 -4
  34. data/lib/contrast/utils/boolean_util.rb +0 -30
  35. data/lib/contrast/utils/gemfile_reader.rb +0 -193
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2172066d6736b55c6754bb6913ec9fb9962ac1b818a85b4faa7ef822bb5df97
4
- data.tar.gz: 209286f4ef6ce8b688e3849a502ece6cfc914f795fae25b5cb417b3fa3998b50
3
+ metadata.gz: ce67e5e10275cf1425bc05d510e6a2b0b436f90c180027bfce2abfeccf11e86e
4
+ data.tar.gz: 5644a75c92b7502f285030c83f3469820c6efdf8c582eb32328d8aefe2f4821a
5
5
  SHA512:
6
- metadata.gz: e6c19a309c1d7c7e2600d2f90d5da2664c315550be00475720165dde741d821d3ceb391282831aeb8ddcbe8e86b50b48d741d5c63d85c7a92c38ef0e54b7b0cd
7
- data.tar.gz: f4c1a92e5272730285b467c63768e31b1d6d7cb4266cbd6133c6de312603fcabc9c3bc814bc7f20d48fa444651fb040713ed1438b70076e9be9be396dab6603b
6
+ metadata.gz: c777194316965c247ced91d30731f9d263db482975fe4702d20332f224ef17c1f23ed281066b07398a900b5f5561019fe15ef6bb882f64c38efb05a693100073
7
+ data.tar.gz: 6a3cf5245ba8448d4023b9e29e608058747dc66c7f1296265d1b2f589ee55bedabe192058eb638839667546acb14650397a35ae36127a0ce316d2a8c876be3a8
@@ -23,7 +23,6 @@ require 'contrast/extension/protect'
23
23
  require 'contrast/extension/protect/kernel'
24
24
 
25
25
  require 'contrast/utils/object_share'
26
- require 'contrast/utils/boolean_util'
27
26
  require 'contrast/utils/string_utils'
28
27
  require 'contrast/utils/io_util'
29
28
  require 'contrast/utils/os'
@@ -88,8 +87,8 @@ require 'contrast/agent/assess'
88
87
  # protect rules
89
88
  require 'contrast/agent/protect/rule'
90
89
 
91
- # application libraries
92
- require 'contrast/utils/gemfile_reader'
90
+ # application libraries and technologies
91
+ require 'contrast/agent/inventory'
93
92
 
94
93
  # rack event monitoring
95
94
  require 'contrast/agent/middleware'
@@ -17,22 +17,33 @@ module Contrast
17
17
  access_component :analysis
18
18
 
19
19
  class << self
20
+ # Use the given trace_point, built from an :end event, to determine
21
+ # where the loaded code lives and scan that code for policy
22
+ # violations.
23
+ #
24
+ # @param trace_point [TracePoint] the TracePoint generated by an
25
+ # :end event at the end of a Module definition.
20
26
  def scan trace_point
21
27
  return unless ASSESS.enabled?
22
28
  return unless ASSESS.require_scan?
23
29
 
30
+ provider_values = policy.providers.values
31
+ return if provider_values.all?(&:disabled?)
32
+
24
33
  return unless trace_point.path
25
34
  return if trace_point.path.start_with?(Gem.dir)
26
35
 
27
36
  mod = trace_point.self
28
37
  return if mod.cs__frozen? || mod.singleton_class?
29
38
 
30
- # TODO: RUBY-1013 - get AST here instead of TP, so we only need
31
- # to make one per provider, instead of one per rule
32
- policy.providers.each_value do |provider|
33
- if RUBY_VERSION >= '2.6.0'
34
- provider.parse(trace_point)
35
- else # TODO: RUBY-1014 - remove alternative
39
+ # TODO: RUBY-1014 - remove non-AST approach
40
+ if RUBY_VERSION >= '2.6.0'
41
+ ast = RubyVM::AbstractSyntaxTree.parse_file(trace_point.path)
42
+ provider_values.each do |provider|
43
+ provider.parse(trace_point, ast)
44
+ end
45
+ else
46
+ provider_values.each do |provider|
36
47
  provider.analyze(mod)
37
48
  end
38
49
  end
@@ -85,10 +85,11 @@ module Contrast
85
85
  #
86
86
  # @param trace_point [TracePoint] the TracePoint event created on
87
87
  # the :end of a Module being loaded
88
- def parse trace_point
88
+ # @param ast [RubyVM::AbstractSyntaxTree::Node] the abstract syntax
89
+ # tree of the Module defined in the TracePoint end event
90
+ def parse trace_point, ast
89
91
  return if disabled?
90
92
 
91
- ast = RubyVM::AbstractSyntaxTree.parse_file(trace_point.path)
92
93
  parse_ast(trace_point.self, ast)
93
94
  rescue StandardError => e
94
95
  logger.error('Unable to parse AST for hardcoded keys', e, module: trace_point.self)
@@ -0,0 +1,15 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Agent
6
+ # Namespace used for inventory behavior
7
+ module Inventory
8
+ end
9
+ end
10
+ end
11
+
12
+ require 'contrast/agent/inventory/dependencies'
13
+ require 'contrast/agent/inventory/gemfile_digest_cache'
14
+ require 'contrast/agent/inventory/dependency_usage_analysis'
15
+ require 'contrast/agent/inventory/dependency_analysis'
@@ -0,0 +1,50 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Agent
6
+ module Inventory
7
+ # this module is included in classes that need access to the applications dependencies
8
+ module Dependencies
9
+ CONTRAST_AGENT = 'contrast-agent'
10
+
11
+ # the #clone is necessary here, as a require in another thread could
12
+ # potentially result in adding a key to the loaded_specs hash during
13
+ # iteration. (as in RUBY-330)
14
+ # this takes care of filtering out contrast-only dependencies
15
+ def loaded_specs
16
+ specs = Gem.loaded_specs.clone
17
+ specs.delete_if { |name, _v| contrast?(name) }
18
+ end
19
+
20
+ private
21
+
22
+ def contrast_gems
23
+ @_contrast_gems ||= find_contrast_gems
24
+ end
25
+
26
+ def contrast? name
27
+ contrast_gems.include?(name)
28
+ end
29
+
30
+ # Go through all dependents, given as a pair from the DependencyList: `dependency`
31
+ # is the dependency itself, filled with all its specs. `dependents` is the array of reverse
32
+ # dependencies for the aforementioned dependency. If the dependency is also in contrast_dep_set,
33
+ # then contrast depends on it. If its array of dependents is 1, then contrast is the
34
+ # only dependency in that list. Since only contrast depends on it, we should ignore it.
35
+ def find_contrast_gems
36
+ ignore = Set.new([CONTRAST_AGENT])
37
+ contrast_specs = Gem::DependencyList.from_specs.specs.find do |dependency|
38
+ dependency.name == CONTRAST_AGENT
39
+ end
40
+ contrast_dep_set = contrast_specs.dependencies.map(&:name).to_set
41
+
42
+ Gem::DependencyList.from_specs.spec_predecessors.each_pair do |dependency, dependents|
43
+ ignore.add(dependency.name) if contrast_dep_set.include?(dependency.name) && dependents.length == 1
44
+ end
45
+ ignore
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/inventory/dependencies'
5
+ require 'contrast/components/interface'
6
+ require 'contrast/utils/object_share'
7
+
8
+ module Contrast
9
+ module Agent
10
+ module Inventory
11
+ # Used to collect dependencies of the application for reporting
12
+ class DependencyAnalysis
13
+ include Singleton
14
+ include Contrast::Agent::Inventory::Dependencies
15
+ include Contrast::Components::Interface
16
+
17
+ access_component :analysis
18
+
19
+ # Report the dependencies of this application
20
+ #
21
+ # @return [Array<Contrast::Api::Dtm::Library>] protobuf form of the
22
+ # Gem::Specification that have been loaded for this application.
23
+ def library_pb_list
24
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless INVENTORY.enabled?
25
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless INVENTORY.analyze_libraries?
26
+
27
+ loaded_specs.each_with_object([]) do |(_name, spec), reported_lib_list|
28
+ next unless spec
29
+ next unless (digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec))
30
+
31
+ reported_lib_list << Contrast::Api::Dtm::Library.build(digest, spec)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/inventory/gemfile_digest_cache'
5
+ require 'contrast/agent/inventory/dependencies'
6
+ require 'contrast/components/interface'
7
+ require 'contrast/utils/object_share'
8
+
9
+ module Contrast
10
+ module Agent
11
+ module Inventory
12
+ # Used to analyze class usage for reporting
13
+ class DependencyUsageAnalysis
14
+ include Singleton
15
+ include Contrast::Components::Interface
16
+ include Contrast::Agent::Inventory::Dependencies
17
+
18
+ access_component :analysis, :config, :logging
19
+
20
+ def initialize
21
+ return unless enabled?
22
+
23
+ @gemdigest_cache = Contrast::Agent::Inventory::GemfileDigestCache.new
24
+ end
25
+
26
+ # This method is invoked once, along with the rest of our catchup code
27
+ # to report libraries and their associated files that have already been loaded pre-contrast
28
+ def catchup
29
+ return unless enabled?
30
+
31
+ loaded_specs.each do |_name, spec|
32
+ # Get a digest of the Gem file itself
33
+ next unless (digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec))
34
+
35
+ @gemdigest_cache.use_cache(digest) do |existing_files|
36
+ loaded_files_from_gem = $LOADED_FEATURES.select { |f| f.start_with?(spec.full_gem_path) }
37
+ loaded_files_from_gem.each do |file_path|
38
+ logger.trace('Recording loaded file for inventory analysis', line: file_path)
39
+ existing_files << adjust_path_for_reporting(file_path, spec)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # This method is invoked once per TracePoint :end - to map a specific
46
+ # file being required to the gem it belongs to
47
+ #
48
+ # @param path [String] the result of TracePoint#path from the :end
49
+ # event in which the Module was defined
50
+ def associate_file path
51
+ return unless enabled?
52
+
53
+ spec_lookup_path = adjust_path_for_spec_lookup(path)
54
+
55
+ spec = Gem::Specification.find_by_path(spec_lookup_path)
56
+ unless spec
57
+ logger.debug('Unable to resolve gem spec for path', path: path)
58
+ return
59
+ end
60
+
61
+ digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec)
62
+ unless digest
63
+ logger.debug('Unable to resolve digest for gem spec', spec: spec.to_s)
64
+ return
65
+ end
66
+ report_path = adjust_path_for_reporting(path, spec)
67
+ @gemdigest_cache.get(digest) << report_path
68
+ rescue StandardError => e
69
+ logger.error('Unable to inventory file path', e, path: path)
70
+ end
71
+
72
+ # Populate the library_usages filed of the Activity message using the
73
+ # data stored in the @gemdigest_cache
74
+ #
75
+ # @param activity [Contrast::Api::Dtm::Activity] the message to which
76
+ # to append the usage data
77
+ def generate_library_usage activity
78
+ return unless enabled?
79
+ return if @gemdigest_cache.empty?
80
+
81
+ @gemdigest_cache.generate_usage_data(activity)
82
+ end
83
+
84
+ private
85
+
86
+ def adjust_path_for_spec_lookup path
87
+ idx = path.index('/lib/')
88
+ path = path[(idx + 4)..-1] if idx
89
+ path
90
+ end
91
+
92
+ def adjust_path_for_reporting path, gem_spec
93
+ path.delete_prefix(gem_spec.full_gem_path)
94
+ end
95
+
96
+ # We only use this if inventory and library analysis are enabled
97
+ def enabled?
98
+ @_enabled = INVENTORY.enabled? && INVENTORY.analyze_libraries? if @_enabled.nil?
99
+ @_enabled
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Agent
6
+ module Inventory
7
+ # Keeps a map of gem digest to files for reporting file usage
8
+ class GemfileDigestCache
9
+ extend Forwardable
10
+ def_delegator :@gem_spec_digest_to_files,
11
+ :empty?
12
+
13
+ def initialize
14
+ @gem_spec_digest_to_files = {}
15
+ end
16
+
17
+ def generate_usage_data activity
18
+ return unless activity
19
+
20
+ @gem_spec_digest_to_files.each_pair do |digest, files|
21
+ usage = Contrast::Api::Dtm::LibraryUsageUpdate.build(digest, files)
22
+ activity.library_usages[usage.hash_code] = usage if activity
23
+ end
24
+ @gem_spec_digest_to_files.clear
25
+ end
26
+
27
+ def use_cache digest
28
+ yield get(digest)
29
+ end
30
+
31
+ def get digest
32
+ @gem_spec_digest_to_files[digest] = Set.new unless @gem_spec_digest_to_files.key?(digest)
33
+ @gem_spec_digest_to_files[digest]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -94,8 +94,7 @@ module Contrast
94
94
  if CONFIG.invalid?
95
95
  AGENT.disable!
96
96
  logger.error('!!! CONFIG FILE IS INVALID - DISABLING CONTRAST AGENT !!!')
97
- elsif CONFIG.disabled?
98
- AGENT.disable!
97
+ elsif AGENT.disabled?
99
98
  logger.warn('Contrast disabled by configuration. Continuing without instrumentation.')
100
99
  else
101
100
  AGENT.enable!
@@ -26,7 +26,7 @@ module Contrast
26
26
 
27
27
  action = properties['action']
28
28
  write_marker = write?(action, *args)
29
- possible_write = write_marker && possible_write(write_marker)
29
+ possible_write = write_marker && possible_write?(write_marker)
30
30
  path_traversal_rule(path, possible_write, object, method)
31
31
 
32
32
  # If the action was copy, we need to handle the write half of it.
@@ -48,7 +48,7 @@ module Contrast
48
48
 
49
49
  private
50
50
 
51
- def possible_write input
51
+ def possible_write? input
52
52
  input.cs__respond_to?(:to_s) &&
53
53
  input.to_s.include?(Contrast::Utils::ObjectShare::WRITE_FLAG)
54
54
  end
@@ -62,7 +62,7 @@ module Contrast
62
62
  return true if action == WRITE
63
63
 
64
64
  write_marker = args.length > 1 ? args[1] : nil
65
- write_marker && possible_write(write_marker)
65
+ write_marker && possible_write?(write_marker)
66
66
  end
67
67
 
68
68
  def path_traversal_rule path, possible_write, object, method
@@ -83,6 +83,7 @@ module Contrast
83
83
  tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" }
84
84
  gems = ENV['GEM_PATH']
85
85
  tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems
86
+ tmp.map!(&:downcase)
86
87
  tmp
87
88
  else
88
89
  []
@@ -18,7 +18,7 @@ module Contrast
18
18
  end
19
19
 
20
20
  def send_activity_messages
21
- Contrast::Utils::GemfileReader.instance.generate_library_usage(context.activity)
21
+ Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.generate_library_usage(context.activity)
22
22
  [context.server_activity, context.activity, context.observed_route].each do |message|
23
23
  Contrast::Agent.messaging_queue.send_event_eventually(message)
24
24
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/components/interface'
5
- require 'contrast/utils/gemfile_reader'
5
+ require 'contrast/agent/inventory'
6
6
  require 'contrast/api/decorators/application_update'
7
7
 
8
8
  module Contrast
@@ -18,7 +18,7 @@ module Contrast
18
18
  def catchup
19
19
  @_catchup ||= begin
20
20
  with_contrast_scope do
21
- Contrast::Utils::GemfileReader.instance.map_loaded_classes
21
+ Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.catchup
22
22
  send_inventory_message
23
23
  true
24
24
  end
@@ -35,7 +35,7 @@ module Contrast
35
35
  path = tracepoint_event.path
36
36
  return if path&.include?('contrast')
37
37
 
38
- Contrast::Utils::InventoryUtil.inventory_class(path) if path
38
+ Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.associate_file(path) if path
39
39
  Contrast::Agent::Patching::Policy::Patcher.patch_specific_module(loaded_module)
40
40
  Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolation(loaded_module)
41
41
  Contrast::Agent::Assess::Policy::PolicyScanner.scan(tracepoint_event)
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '3.16.0'
6
+ VERSION = '4.0.0'
7
7
  end
8
8
  end
@@ -8,11 +8,14 @@ module Contrast
8
8
  end
9
9
  end
10
10
  end
11
+
11
12
  require 'contrast/api/decorators/message'
12
13
  require 'contrast/api/decorators/application_update'
13
14
  require 'contrast/api/decorators/input_analysis'
14
15
  require 'contrast/api/decorators/application_settings'
15
16
  require 'contrast/api/decorators/server_features'
17
+ require 'contrast/api/decorators/library'
18
+ require 'contrast/api/decorators/library_usage_update'
16
19
  require 'contrast/api/decorators/route_coverage'
17
20
  require 'contrast/api/decorators/trace_event_object'
18
21
  require 'contrast/api/decorators/trace_event_signature'
@@ -21,7 +21,6 @@ module Contrast
21
21
  module ClassMethods
22
22
  include Contrast::Components::Interface
23
23
  access_component :logging
24
-
25
24
  # receiver is memoized because it is the address/host/port of the server, once we
26
25
  # resolve this for the first time, it shouldn't change
27
26
  #
@@ -44,7 +44,7 @@ module Contrast
44
44
  msg = new
45
45
  msg.append_route_coverage_data(Contrast::Agent.framework_manager.find_route_discovery_data)
46
46
  msg.append_platform_version(Contrast::Agent.framework_manager.platform_version)
47
- msg.append_library_update(Contrast::Utils::GemfileReader.instance.library_pb_list)
47
+ msg.append_library_update(Contrast::Agent::Inventory::DependencyAnalysis.instance.library_pb_list)
48
48
  msg
49
49
  end
50
50
  end
@@ -0,0 +1,53 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/string_utils'
5
+ require 'contrast/utils/sha256_builder'
6
+ require 'yaml'
7
+
8
+ module Contrast
9
+ module Api
10
+ module Decorators
11
+ # Used to decorate the Library protobuf model to handle Gem::Specification translation
12
+ module Library
13
+ def self.included klass
14
+ klass.extend(ClassMethods)
15
+ end
16
+ # Used to add class methods to the Library class on inclusion of the decorator
17
+ module ClassMethods
18
+ def build digest, gem_specification
19
+ msg = new
20
+ msg.file_path = Contrast::Utils::StringUtils.force_utf8(gem_specification.name)
21
+ msg.hash_code = Contrast::Utils::StringUtils.force_utf8(digest)
22
+ msg.version = Contrast::Utils::StringUtils.force_utf8(gem_specification.version)
23
+ msg.manifest = Contrast::Utils::StringUtils.force_utf8(build_manifest(gem_specification))
24
+ msg.external_ms = date_to_ms(gem_specification.date)
25
+ msg.internal_ms = msg.external_ms
26
+ msg.url = Contrast::Utils::StringUtils.force_utf8(gem_specification.homepage)
27
+ msg.class_count = file_count(gem_specification.full_gem_path.to_s)
28
+ msg.used_class_count = 0
29
+ msg
30
+ end
31
+
32
+ # These are all the code files that are located in the Gem directory loaded
33
+ # by the current environment; this includes more than Ruby files
34
+ def file_count path
35
+ Contrast::Utils::Sha256Builder.instance.files(path).length
36
+ end
37
+
38
+ def build_manifest spec
39
+ Contrast::Utils::StringUtils.force_utf8(spec.to_yaml.to_s)
40
+ rescue StandardError
41
+ nil
42
+ end
43
+
44
+ def date_to_ms date
45
+ (date.to_f * 1000.0).to_i
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ Contrast::Api::Dtm::Library.include(Contrast::Api::Decorators::Library)
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/string_utils'
5
+
6
+ module Contrast
7
+ module Api
8
+ module Decorators
9
+ # Used to decorate the LibraryUsageUpdate protobuf
10
+ module LibraryUsageUpdate
11
+ def self.included klass
12
+ klass.extend(ClassMethods)
13
+ end
14
+ # Used to add class methods to the LibraryUsageUpdate class on inclusion of the decorator
15
+ module ClassMethods
16
+ def build digest, files
17
+ msg = new
18
+ msg.hash_code = Contrast::Utils::StringUtils.force_utf8(digest)
19
+ files.each do |required_file|
20
+ msg.class_names[required_file] = true
21
+ end
22
+ msg
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Contrast::Api::Dtm::LibraryUsageUpdate.include(Contrast::Api::Decorators::LibraryUsageUpdate)
@@ -17,7 +17,8 @@ module Contrast
17
17
  access_component :analysis, :config, :settings
18
18
 
19
19
  def enabled?
20
- !!@enabled
20
+ @_enabled = !false?(CONFIG.root.enable) if @_enabled.nil?
21
+ @_enabled
21
22
  end
22
23
 
23
24
  def disabled?
@@ -25,11 +26,11 @@ module Contrast
25
26
  end
26
27
 
27
28
  def enable!
28
- @enabled = true
29
+ @_enabled = true
29
30
  end
30
31
 
31
32
  def disable!
32
- @enabled = false
33
+ @_enabled = false
33
34
  end
34
35
 
35
36
  def ruleset
@@ -49,12 +50,12 @@ module Contrast
49
50
  end
50
51
 
51
52
  def patch_yield?
52
- @_patch_yield = !Contrast::Utils::BooleanUtil.false?(CONFIG.root.agent.ruby.propagate_yield) if @_patch_yield.nil?
53
+ @_patch_yield = !false?(CONFIG.root.agent.ruby.propagate_yield) if @_patch_yield.nil?
53
54
  @_patch_yield
54
55
  end
55
56
 
56
57
  def interpolation_enabled?
57
- @_interpolation_enabled = !Contrast::Utils::BooleanUtil.false?(CONFIG.root.agent.ruby.interpolate) if @_interpolation_enabled.nil?
58
+ @_interpolation_enabled = !false?(CONFIG.root.agent.ruby.interpolate) if @_interpolation_enabled.nil?
58
59
  @_interpolation_enabled
59
60
  end
60
61
 
@@ -1,9 +1,7 @@
1
1
  # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'contrast/utils/boolean_util'
5
4
  require 'contrast/utils/env_configuration_item'
6
- require 'contrast/utils/object_share'
7
5
  require 'contrast/configuration'
8
6
 
9
7
  module Contrast
@@ -39,41 +37,9 @@ module Contrast
39
37
  end
40
38
  alias_method :rebuild, :build
41
39
 
42
- # Prefer abstraction, but use #raw if you need.
43
- # grep 'CONFIG.raw' for opportunities to refactor.
44
- def raw
45
- @config
46
- end
47
-
40
+ # @return [Contrast::Config::RootConfiguration]
48
41
  def root
49
- raw.root
50
- end
51
-
52
- def enabled?
53
- @_enabled = !Contrast::Utils::BooleanUtil.false?(raw.enable) if @_enabled.nil?
54
- @_enabled
55
- end
56
-
57
- def disabled?
58
- !enabled?
59
- end
60
-
61
- def protect?
62
- @_protect = Contrast::Utils::BooleanUtil.true?(raw.protect.enable) if @_protect.nil?
63
- @_protect
64
- end
65
-
66
- def assess?
67
- @_assess = Contrast::Utils::BooleanUtil.true?(raw.assess.enable) if @_assess.nil?
68
- @_assess
69
- end
70
-
71
- def session_id
72
- @_session_id ||= raw.application.session_id
73
- end
74
-
75
- def session_metadata
76
- @_session_metadata ||= raw.application.session_metadata
42
+ @config.root
77
43
  end
78
44
 
79
45
  def valid?
@@ -84,6 +50,10 @@ module Contrast
84
50
  !valid?
85
51
  end
86
52
 
53
+ def loggable
54
+ @config.loggable
55
+ end
56
+
87
57
  private
88
58
 
89
59
  SESSION_VARIABLES = "Invalid configuration. Setting both application.session_id and application.session_metadata is not allowed.\n"
@@ -111,9 +81,31 @@ module Contrast
111
81
  next unless env_key.to_s.start_with?(CONTRAST_ENV_MARKER)
112
82
 
113
83
  config_item = Contrast::Utils::EnvConfigurationItem.new(env_key, env_value)
114
- raw.assign_value_to_path_array(config_item.dot_path_array, config_item.value)
84
+ @config.assign_value_to_path_array(config_item.dot_path_array, config_item.value)
115
85
  end
116
86
  end
87
+
88
+ # Typically, this would be accessed through
89
+ # Contrast::Components::AppContext, but we're too early in the
90
+ # initialization of the Agent to use that mechanism, so we look it up
91
+ # directly for ourselves
92
+ #
93
+ # @return [String,nil] the value of the session id set in the
94
+ # configuration, or nil if unset
95
+ def session_id
96
+ @config.application.session_id
97
+ end
98
+
99
+ # Typically, this would be accessed through
100
+ # Contrast::Components::AppContext, but we're too early in the
101
+ # initialization of the Agent to use that mechanism, so we look it up
102
+ # directly for ourselves
103
+ #
104
+ # @return [String,nil] the value of the session metadata set in the
105
+ # configuration, or nil if unset
106
+ def session_metadata
107
+ @config.application.session_metadata
108
+ end
117
109
  end
118
110
 
119
111
  COMPONENT_INTERFACE = Interface.new
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'delegate'
5
5
  require 'contrast/extension/module'
6
- require 'contrast/utils/boolean_util'
6
+ require 'contrast/utils/object_share'
7
7
 
8
8
  module Contrast
9
9
  # This is the base module for our components classes. It is intended to
@@ -49,12 +49,34 @@ module Contrast
49
49
  end
50
50
 
51
51
  module Methods # :nodoc:
52
+ # use this to determine if the configuration value is literally boolean
53
+ # false or some form of the word `false`, regardless of case. It should
54
+ # be used for those values which default to `true` as they should only
55
+ # treat a value explicitly set to `false` as such.
56
+ #
57
+ # @param config_param [Boolean,String] the value to check
58
+ # @return [Boolean] should the value be treated as `false`
52
59
  def false? config_param
53
- Contrast::Utils::BooleanUtil.false?(config_param)
60
+ return false if config_param == true
61
+ return true if config_param == false
62
+ return false unless config_param.cs__is_a?(String)
63
+
64
+ Contrast::Utils::ObjectShare::FALSE.casecmp?(config_param)
54
65
  end
55
66
 
67
+ # use this to determine if the configuration value is literally boolean
68
+ # true or some form of the word `true`, regardless of case. It should
69
+ # be used for those values which default to `false` as they should only
70
+ # treat a value explicitly set to `true` as such.
71
+ #
72
+ # @param config_param [Boolean,String] the value to check
73
+ # @return [Boolean] should the value be treated as `true`
56
74
  def true? config_param
57
- Contrast::Utils::BooleanUtil.true?(config_param)
75
+ return false if config_param == false
76
+ return true if config_param == true
77
+ return false unless config_param.cs__is_a?(String)
78
+
79
+ Contrast::Utils::ObjectShare::TRUE.casecmp?(config_param)
58
80
  end
59
81
  end
60
82
  end
@@ -13,12 +13,17 @@ module Contrast
13
13
  include Contrast::Components::ComponentBase
14
14
  include Contrast::Components::Interface
15
15
 
16
- access_component :config
16
+ access_component :config, :settings
17
17
 
18
18
  def enabled?
19
19
  @_enabled = !false?(CONFIG.root.inventory.enable) if @_enabled.nil?
20
20
  @_enabled
21
21
  end
22
+
23
+ def analyze_libraries?
24
+ @_analyze_libraries = !false?(CONFIG.root.inventory.analyze_libraries) if @_analyze_libraries.nil?
25
+ @_analyze_libraries
26
+ end
22
27
  end
23
28
 
24
29
  COMPONENT_INTERFACE = Interface.new
@@ -7,8 +7,8 @@ module Contrast
7
7
  # inventory functionality of the Agent.
8
8
  class InventoryConfiguration < BaseConfiguration
9
9
  KEYS = {
10
- enable: EMPTY_VALUE,
11
- record_used_classes: EMPTY_VALUE,
10
+ enable: Contrast::Config::DefaultValue.new(true),
11
+ analyze_libraries: Contrast::Config::DefaultValue.new(true),
12
12
  tags: EMPTY_VALUE
13
13
  }.cs__freeze
14
14
 
@@ -45,6 +45,9 @@ module Contrast
45
45
  find_all_routes(::Rails.application, [])
46
46
  end
47
47
 
48
+ # Find the current route, based on the provided Request wrapper
49
+ # @param request[Contrast::Agent::Request]
50
+ # @return [Contrast::Api::Dtm::RouteCoverage]
48
51
  def current_route request
49
52
  return unless ::Rails.cs__respond_to?(:application)
50
53
 
@@ -33,7 +33,7 @@ module Contrast
33
33
  def application_configuration
34
34
  return unless info?
35
35
 
36
- loggable = CONFIG.raw.loggable
36
+ loggable = CONFIG.loggable
37
37
  info('Current configuration', configuration: loggable)
38
38
  env_keys = ENV.keys.select { |env_key| env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER) }
39
39
  env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
@@ -3,7 +3,6 @@
3
3
 
4
4
  require 'contrast/utils/timer'
5
5
  require 'contrast/utils/object_share'
6
- require 'contrast/utils/gemfile_reader'
7
6
  require 'contrast/components/interface'
8
7
 
9
8
  module Contrast
@@ -25,12 +24,6 @@ module Contrast
25
24
  DEFAULT = 'default'
26
25
  LOCALHOST = 'localhost'
27
26
 
28
- def self.inventory_class class_path
29
- Contrast::Utils::GemfileReader.instance.map_class(class_path)
30
- rescue StandardError => e
31
- logger.error('Unable to inventory module', e, path: class_path)
32
- end
33
-
34
27
  def self.active_record_config
35
28
  return @_active_record_config if instance_variable_defined?(:@_active_record_config)
36
29
 
@@ -52,18 +52,6 @@ module Contrast
52
52
  parent_dir = File.dirname(gems_dir)
53
53
  File.join(parent_dir, Contrast::Utils::ObjectShare::CACHE)
54
54
  end
55
-
56
- def self.files path
57
- instance.files(path)
58
- end
59
-
60
- def self.sha256 path
61
- instance.sha256(path)
62
- end
63
-
64
- def self.build_from_spec spec
65
- instance.build_from_spec(spec)
66
- end
67
55
  end
68
56
  end
69
57
  end
@@ -1 +1 @@
1
- 2.14.3
1
+ 2.14.4
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrast-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.16.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - galen.palmer@contrastsecurity.com
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: exe
14
14
  cert_chain: []
15
- date: 2020-10-26 00:00:00.000000000 Z
15
+ date: 2020-11-05 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: amazing_print
@@ -770,6 +770,11 @@ files:
770
770
  - lib/contrast/agent/deadzone/policy/policy.rb
771
771
  - lib/contrast/agent/disable_reaction.rb
772
772
  - lib/contrast/agent/exclusion_matcher.rb
773
+ - lib/contrast/agent/inventory.rb
774
+ - lib/contrast/agent/inventory/dependencies.rb
775
+ - lib/contrast/agent/inventory/dependency_analysis.rb
776
+ - lib/contrast/agent/inventory/dependency_usage_analysis.rb
777
+ - lib/contrast/agent/inventory/gemfile_digest_cache.rb
773
778
  - lib/contrast/agent/inventory/policy/datastores.rb
774
779
  - lib/contrast/agent/inventory/policy/policy.rb
775
780
  - lib/contrast/agent/inventory/policy/trigger_node.rb
@@ -847,6 +852,8 @@ files:
847
852
  - lib/contrast/api/decorators/application_update.rb
848
853
  - lib/contrast/api/decorators/http_request.rb
849
854
  - lib/contrast/api/decorators/input_analysis.rb
855
+ - lib/contrast/api/decorators/library.rb
856
+ - lib/contrast/api/decorators/library_usage_update.rb
850
857
  - lib/contrast/api/decorators/message.rb
851
858
  - lib/contrast/api/decorators/rasp_rule_sample.rb
852
859
  - lib/contrast/api/decorators/route_coverage.rb
@@ -941,11 +948,9 @@ files:
941
948
  - lib/contrast/tasks/service.rb
942
949
  - lib/contrast/utils/assess/sampling_util.rb
943
950
  - lib/contrast/utils/assess/tracking_util.rb
944
- - lib/contrast/utils/boolean_util.rb
945
951
  - lib/contrast/utils/class_util.rb
946
952
  - lib/contrast/utils/duck_utils.rb
947
953
  - lib/contrast/utils/env_configuration_item.rb
948
- - lib/contrast/utils/gemfile_reader.rb
949
954
  - lib/contrast/utils/hash_digest.rb
950
955
  - lib/contrast/utils/heap_dump_util.rb
951
956
  - lib/contrast/utils/invalid_configuration_util.rb
@@ -1,30 +0,0 @@
1
- # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
- # frozen_string_literal: true
3
-
4
- require 'contrast/utils/object_share'
5
-
6
- module Contrast
7
- module Utils
8
- # Utility methods for asserting truthy or falsy state of a value expected
9
- # to equate to a boolean
10
- class BooleanUtil
11
- class << self
12
- def false? config
13
- return false if config == true
14
- return true if config == false
15
- return false unless config.cs__is_a?(String)
16
-
17
- Contrast::Utils::ObjectShare::FALSE.casecmp?(config)
18
- end
19
-
20
- def true? config
21
- return false if config == false
22
- return true if config == true
23
- return false unless config.cs__is_a?(String)
24
-
25
- Contrast::Utils::ObjectShare::TRUE.casecmp?(config)
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,193 +0,0 @@
1
- # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
- # frozen_string_literal: true
3
-
4
- require 'set'
5
- require 'contrast/utils/sha256_builder'
6
- require 'contrast/utils/string_utils'
7
- require 'contrast/components/interface'
8
- require 'contrast/api'
9
-
10
- module Contrast
11
- module Utils
12
- # GemfileReader has methods for extracting information from gem specs
13
- # it also has a cache of library file digests to the lines of code found.
14
- class GemfileReader
15
- include Singleton
16
- include Contrast::Components::Interface
17
-
18
- access_component :config, :logging
19
-
20
- CONTRAST_AGENT = 'contrast-agent'
21
-
22
- def initialize
23
- # Map of a Gem's Spec Digest to all loaded files from that Gem
24
- @spec_to_files = {}
25
- end
26
-
27
- # the #clone is necessary here, as a require in another thread could
28
- # potentially result in adding a key to the loaded_specs hash during
29
- # iteration. (as in RUBY-330)
30
- def loaded_specs
31
- Gem.loaded_specs.clone
32
- end
33
-
34
- # indicates if there's been an update to library information, allowing us
35
- # to only serialize this information on change.
36
- def updated?
37
- @updated
38
- end
39
-
40
- def updated!
41
- @updated = true
42
- end
43
-
44
- # Once we're Contrasted, we intercept require calls to do inventory.
45
- # In order to catch up, we do a one-time manual catchup, & inventory
46
- # all the already-loaded gems.
47
- def map_loaded_classes
48
- loaded_specs.each do |name, spec|
49
- # Don't count Contrast gems
50
- next if contrast_gems.include? name
51
-
52
- # Get a digest of the Gem file itself
53
- next unless (digest = Contrast::Utils::Sha256Builder.build_from_spec(spec))
54
-
55
- paths = get_by_digest(digest)
56
- path = spec.full_gem_path
57
- $LOADED_FEATURES.each do |line|
58
- next unless line.cs__is_a?(String)
59
- next unless line.start_with?(path)
60
-
61
- logger.trace('Recording loaded gem for inventory analysis', line: line)
62
- updated!
63
- paths << adjust_lib(line)
64
- end
65
- end
66
- end
67
-
68
- def map_class path
69
- path = adjust_lib(path)
70
-
71
- return unless (spec = Gem::Specification.find_by_path(path))
72
- return unless (digest = Contrast::Utils::Sha256Builder.build_from_spec(spec))
73
-
74
- updated!
75
- get_by_digest(digest) << path
76
- end
77
-
78
- def library_pb_list
79
- loaded_specs.each_with_object([]) do |(name, spec), arr|
80
- next if contrast_gems.include? name
81
- next unless spec
82
- next unless (digest = Contrast::Utils::Sha256Builder.build_from_spec(spec))
83
-
84
- arr << build_library_pb(digest, spec)
85
- end
86
- end
87
-
88
- def generate_library_usage activity = nil
89
- return unless updated?
90
-
91
- @spec_to_files.each_pair do |digest, files|
92
- usage = Contrast::Api::Dtm::LibraryUsageUpdate.new
93
- usage.hash_code = Contrast::Utils::StringUtils.force_utf8(digest)
94
- activity.library_usages[usage.hash_code] = usage if activity
95
- # TODO: RUBY-882 once TS switches to take filenames, remove the count setter and
96
- # send the class names in usage.class_names
97
- usage.count = files.size
98
- end
99
- # TODO: RUBY-882 once TS switches to take filenames, clear this and remove the
100
- # @updated variable
101
-
102
- # @spec_to_files.clear
103
- @updated = false
104
- end
105
-
106
- private
107
-
108
- # marker for the lib dir in an absolute file path. purposefully includes
109
- # the trailing '/'
110
- LIB = '/lib/'
111
-
112
- # Kernel#load uses the absolute path, but Gems / Specs use the path after
113
- # `/lib`. This method accounts for that and trims out the `/lib/` section
114
- # and starts with the first `/` after and the trailing file extension, if
115
- # present.
116
- #
117
- # @param path [String] the path to parse
118
- # @return [String] the relative path of the file, after the lib directory
119
- def adjust_lib path
120
- idx = path.index(LIB)
121
- path = path[(idx + 4)..-1] if idx
122
- idx = path.rindex(Contrast::Utils::ObjectShare::PERIOD)
123
- path = path[0..idx] if idx
124
- path
125
- end
126
-
127
- def get_by_digest digest
128
- @spec_to_files[digest] = Set.new unless @spec_to_files.key?(digest)
129
- @spec_to_files[digest]
130
- end
131
-
132
- def build_library_pb digest, spec
133
- lib = Contrast::Api::Dtm::Library.new
134
- lib.file_path = Contrast::Utils::StringUtils.force_utf8(spec.name)
135
- lib.hash_code = Contrast::Utils::StringUtils.force_utf8(digest)
136
- lib.version = Contrast::Utils::StringUtils.force_utf8(spec.version)
137
- lib.manifest = Contrast::Utils::StringUtils.force_utf8(build_manifest(spec))
138
- lib.external_ms = date_to_ms(spec.date)
139
- lib.internal_ms = lib.external_ms
140
- lib.url = Contrast::Utils::StringUtils.force_utf8(spec.homepage)
141
- # Library tags are appended in the ApplicationUpdate delegator
142
- update_class_counts(lib, digest, spec)
143
- lib
144
- end
145
-
146
- def date_to_ms date
147
- (date.to_f * 1000.0).to_i
148
- end
149
-
150
- def update_class_counts lib, digest, spec
151
- # Updating the class counts
152
- path = spec.full_gem_path.to_s
153
- lib.class_count = all_files(path).length
154
- lib.used_class_count = @spec_to_files.key?(digest) ? get_by_digest(digest).size : 0
155
- lib
156
- end
157
-
158
- def build_manifest spec
159
- Contrast::Utils::StringUtils.force_utf8(spec.to_yaml.to_s) if defined?(YAML)
160
- rescue StandardError
161
- nil
162
- end
163
-
164
- # These are all the code files that are located in the Gem directory loaded
165
- # by the current environment; this includes more than Ruby files
166
- def all_files path
167
- Contrast::Utils::Sha256Builder.instance.files(path)
168
- end
169
-
170
- # Go through all dependents, given as a pair from the DependencyList: `dependency`
171
- # is the dependency itself, filled with all its specs. `dependents` is the array of reverse
172
- # dependencies for the aforementioned dependency. If the dependency is also in contrast_dep_set,
173
- # then contrast depends on it. If its array of dependents is 1, then contrast is the
174
- # only dependency in that list. Since only contrast depends on it, we should ignore it.
175
- def contrast_gems
176
- @_contrast_gems ||= find_contrast_gems
177
- end
178
-
179
- def find_contrast_gems
180
- ignore = Set.new([CONTRAST_AGENT])
181
- contrast_specs = Gem::DependencyList.from_specs.specs.find do |dependency|
182
- dependency.name == CONTRAST_AGENT
183
- end
184
- contrast_dep_set = contrast_specs.dependencies.map(&:name).to_set
185
-
186
- Gem::DependencyList.from_specs.spec_predecessors.each_pair do |dependency, dependents|
187
- ignore.add(dependency.name) if contrast_dep_set.include?(dependency.name) && dependents.length == 1
188
- end
189
- ignore
190
- end
191
- end
192
- end
193
- end