contrast-agent 3.16.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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