inspec 0.33.2 → 0.34.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.
data/lib/fetchers/url.rb CHANGED
@@ -8,21 +8,31 @@ require 'open-uri'
8
8
 
9
9
  module Fetchers
10
10
  class Url < Inspec.fetcher(1)
11
+ MIME_TYPES = {
12
+ 'application/x-zip-compressed' => '.zip',
13
+ 'application/zip' => '.zip',
14
+ 'application/x-gzip' => '.tar.gz',
15
+ 'application/gzip' => '.tar.gz',
16
+ }.freeze
17
+
11
18
  name 'url'
12
19
  priority 200
13
20
 
14
- attr_reader :files
15
-
16
21
  def self.resolve(target, opts = {})
17
- return nil unless target.is_a?(String)
22
+ if target.is_a?(Hash) && target.key?(:url)
23
+ resolve_from_string(target[:url], opts)
24
+ elsif target.is_a?(String)
25
+ resolve_from_string(target, opts)
26
+ end
27
+ end
28
+
29
+ def self.resolve_from_string(target, opts)
18
30
  uri = URI.parse(target)
19
31
  return nil if uri.nil? or uri.scheme.nil?
20
32
  return nil unless %{ http https }.include? uri.scheme
21
33
  target = transform(target)
22
- # fetch this url and hand it off
23
- res = new(target, opts)
24
- resolve_next(res.archive.path, res)
25
- rescue URI::Error => _e
34
+ new(target, opts)
35
+ rescue URI::Error
26
36
  nil
27
37
  end
28
38
 
@@ -44,38 +54,51 @@ module Fetchers
44
54
  # https://github.com/hardening-io/tests-os-hardening/tree/48bd4388ddffde68badd83aefa654e7af3231876
45
55
  # is transformed to
46
56
  # https://github.com/hardening-io/tests-os-hardening/archive/48bd4388ddffde68badd83aefa654e7af3231876.tar.gz
57
+ GITHUB_URL_REGEX = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)(\.git)?(/)?$}
58
+ GITHUB_URL_WITH_TREE_REGEX = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/tree/(?<commit>[\w\.]+)(/)?$}
47
59
  def self.transform(target)
48
- # support for default github url
49
- m = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)(\.git)?(/)?$}.match(target)
50
- return "https://github.com/#{m[:user]}/#{m[:repo]}/archive/master.tar.gz" if m
60
+ transformed_target = if m = GITHUB_URL_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
61
+ "https://github.com/#{m[:user]}/#{m[:repo]}/archive/master.tar.gz"
62
+ elsif m = GITHUB_URL_WITH_TREE_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
63
+ "https://github.com/#{m[:user]}/#{m[:repo]}/archive/#{m[:commit]}.tar.gz"
64
+ end
65
+
66
+ if transformed_target
67
+ Inspec::Log.warn("URL target #{target} transformed to #{transformed_target}. Consider using the git fetcher")
68
+ transformed_target
69
+ else
70
+ target
71
+ end
72
+ end
51
73
 
52
- # support for branch and commit urls
53
- m = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/tree/(?<commit>[\w\.]+)(/)?$}.match(target)
54
- return "https://github.com/#{m[:user]}/#{m[:repo]}/archive/#{m[:commit]}.tar.gz" if m
74
+ attr_reader :files, :archive_path
55
75
 
56
- # if we could not find a match, return the original value
57
- target
76
+ def initialize(url, opts)
77
+ @target = url
78
+ @insecure = opts['insecure']
79
+ @token = opts['token']
80
+ @config = opts
58
81
  end
59
82
 
60
- MIME_TYPES = {
61
- 'application/x-zip-compressed' => '.zip',
62
- 'application/zip' => '.zip',
63
- 'application/x-gzip' => '.tar.gz',
64
- 'application/gzip' => '.tar.gz',
65
- }.freeze
83
+ def fetch(path)
84
+ Inspec::Log.debug("Fetching URL: #{@target}")
85
+ @archive_path = download_archive(path)
86
+ end
87
+
88
+ def resolved_source
89
+ { url: @target }
90
+ end
91
+
92
+ private
66
93
 
67
94
  # download url into archive using opts,
68
95
  # returns File object and content-type from HTTP headers
69
- def self.download_archive(url, opts = {})
96
+ def download_archive(path)
70
97
  http_opts = {}
71
- # http_opts['http_basic_authentication'] = [opts['user'] || '', opts['password'] || ''] if opts['user']
72
- http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if opts['insecure']
73
- http_opts['Authorization'] = "Bearer #{opts['token']}" if opts['token']
98
+ http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if @insecure
99
+ http_opts['Authorization'] = "Bearer #{@token}" if @token
74
100
 
75
- remote = open(
76
- url,
77
- http_opts,
78
- )
101
+ remote = open(@target, http_opts)
79
102
 
80
103
  content_type = remote.meta['content-type']
81
104
  file_type = MIME_TYPES[content_type] ||
@@ -86,25 +109,16 @@ module Fetchers
86
109
  if file_type.nil?
87
110
  fail "Could not determine file type for content type #{content_type}."
88
111
  end
89
-
112
+ final_path = "#{path}#{file_type}"
90
113
  # download content
91
114
  archive = Tempfile.new(['inspec-dl-', file_type])
92
115
  archive.binmode
93
116
  archive.write(remote.read)
94
117
  archive.rewind
95
118
  archive.close
96
- archive
97
- end
98
-
99
- attr_reader :archive
100
-
101
- def initialize(url, opts)
102
- @target = url
103
- @archive = self.class.download_archive(url, opts)
104
- end
105
-
106
- def archive_path
107
- @archive.path
119
+ FileUtils.mv(archive.path, final_path)
120
+ Inspec::Log.debug("Fetched archive moved to: #{final_path}")
121
+ final_path
108
122
  end
109
123
  end
110
124
  end
@@ -62,7 +62,7 @@ EOF
62
62
  def to_yaml
63
63
  {
64
64
  'lockfile_version' => CURRENT_LOCKFILE_VERSION,
65
- 'depends' => @deps,
65
+ 'depends' => @deps.map { |i| stringify_keys(i) },
66
66
  }.to_yaml
67
67
  end
68
68
 
@@ -88,7 +88,33 @@ EOF
88
88
  end
89
89
 
90
90
  def parse_content_hash_0(lockfile_content_hash)
91
- @deps = lockfile_content_hash['depends']
91
+ @deps = if lockfile_content_hash['depends']
92
+ lockfile_content_hash['depends'].map { |i| symbolize_keys(i) }
93
+ end
94
+ end
95
+
96
+ def mutate_hash_keys_with(hash, fun)
97
+ hash.each_with_object({}) do |v, memo|
98
+ key = fun.call(v[0])
99
+ value = if v[1].is_a?(Hash)
100
+ mutate_hash_keys_with(v[1], fun)
101
+ elsif v[1].is_a?(Array)
102
+ v[1].map do |i|
103
+ i.is_a?(Hash) ? mutate_hash_keys_with(i, fun) : i
104
+ end
105
+ else
106
+ v[1]
107
+ end
108
+ memo[key] = value
109
+ end
110
+ end
111
+
112
+ def stringify_keys(hash)
113
+ mutate_hash_keys_with(hash, proc { |i| i.to_s })
114
+ end
115
+
116
+ def symbolize_keys(hash)
117
+ mutate_hash_keys_with(hash, proc { |i| i.to_sym })
92
118
  end
93
119
  end
94
120
  end
@@ -8,7 +8,7 @@ module Inspec
8
8
  # Inspec::Requirement represents a given profile dependency, where
9
9
  # appropriate we delegate to Inspec::Profile directly.
10
10
  #
11
- class Requirement # rubocop:disable Metrics/ClassLength
11
+ class Requirement
12
12
  attr_reader :name, :dep, :cwd, :opts
13
13
  attr_writer :dependencies
14
14
 
@@ -20,12 +20,11 @@ module Inspec
20
20
  end
21
21
 
22
22
  def self.from_lock_entry(entry, cwd, vendor_index, backend)
23
- req = new(entry['name'],
24
- entry['version_constraints'],
23
+ req = new(entry[:name],
24
+ entry[:version_constraints],
25
25
  vendor_index,
26
26
  cwd,
27
- { url: entry['resolved_source'],
28
- backend: backend })
27
+ entry[:resolved_source].merge(backend: backend))
29
28
 
30
29
  locked_deps = []
31
30
  Array(entry['dependencies']).each do |dep_entry|
@@ -59,10 +58,14 @@ module Inspec
59
58
  @dep.match?(name, version)
60
59
  end
61
60
 
61
+ def resolved_source
62
+ @resolved_source ||= fetcher.resolved_source
63
+ end
64
+
62
65
  def to_hash
63
66
  h = {
64
67
  'name' => name,
65
- 'resolved_source' => source_url,
68
+ 'resolved_source' => resolved_source,
66
69
  'version_constraints' => @version_requirement.to_s,
67
70
  }
68
71
 
@@ -70,9 +73,7 @@ module Inspec
70
73
  h['dependencies'] = dependencies.map(&:to_hash)
71
74
  end
72
75
 
73
- if is_vendored?
74
- h['content_hash'] = content_hash
75
- end
76
+ h['content_hash'] = content_hash if content_hash
76
77
  h
77
78
  end
78
79
 
@@ -80,50 +81,21 @@ module Inspec
80
81
  @dependencies = dep_array
81
82
  end
82
83
 
83
- def is_vendored?
84
- @vendor_index.exists?(@name, source_url)
85
- end
86
-
87
84
  def content_hash
88
85
  @content_hash ||= begin
89
- archive_path = @vendor_index.archive_entry_for(@name, source_url)
90
- fail "No vendored archive path for #{self}, cannot take content hash" if archive_path.nil?
91
- Digest::SHA256.hexdigest File.read(archive_path)
86
+ archive_path = @vendor_index.archive_entry_for(fetcher.cache_key) || fetcher.archive_path
87
+ if archive_path && File.file?(archive_path)
88
+ Digest::SHA256.hexdigest File.read(archive_path)
89
+ end
92
90
  end
93
91
  end
94
92
 
95
- def source_url
96
- if opts[:path]
97
- "file://#{File.expand_path(opts[:path], @cwd)}"
98
- elsif opts[:url]
99
- opts[:url]
100
- end
101
- end
102
-
103
- def local_path
104
- @local_path ||= if fetcher.class == Fetchers::Local
105
- File.expand_path(fetcher.target, @cwd)
106
- else
107
- @vendor_index.prefered_entry_for(@name, source_url)
108
- end
109
- end
110
-
111
93
  def fetcher
112
- @fetcher ||= Inspec::Fetcher.resolve(source_url)
94
+ @fetcher ||= Inspec::Fetcher.resolve(opts)
113
95
  fail "No fetcher for #{name} (options: #{opts})" if @fetcher.nil?
114
96
  @fetcher
115
97
  end
116
98
 
117
- def pull
118
- # TODO(ssd): Dispatch on the class here is gross. Seems like
119
- # Fetcher is missing an API we want.
120
- if fetcher.class == Fetchers::Local || @vendor_index.exists?(@name, source_url)
121
- local_path
122
- else
123
- @vendor_index.add(@name, source_url, fetcher.archive_path)
124
- end
125
- end
126
-
127
99
  def dependencies
128
100
  @dependencies ||= profile.metadata.dependencies.map do |r|
129
101
  Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd, backend: @backend)
@@ -131,20 +103,17 @@ module Inspec
131
103
  end
132
104
 
133
105
  def to_s
134
- "#{dep} (#{source_url})"
135
- end
136
-
137
- def path
138
- @path ||= pull
106
+ "#{dep} (#{resolved_source})"
139
107
  end
140
108
 
141
109
  def profile
142
- return nil if path.nil?
143
- opts = { backend: @backend }
110
+ opts = @opts.dup
111
+ opts[:cache] = @vendor_index
112
+ opts[:backend] = @backend
144
113
  if !@dependencies.nil?
145
114
  opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @vendor_index, @backend)
146
115
  end
147
- @profile ||= Inspec::Profile.for_target(path, opts)
116
+ @profile ||= Inspec::Profile.for_target(opts, opts)
148
117
  end
149
118
  end
150
119
  end
@@ -31,7 +31,23 @@ module Inspec
31
31
  new.resolve(reqs)
32
32
  end
33
33
 
34
- def resolve(deps, top_level = true, seen_items = {}, path_string = '')
34
+ def detect_duplicates(deps, top_level, path_string)
35
+ seen_items_local = []
36
+ deps.each do |dep|
37
+ if seen_items_local.include?(dep.name)
38
+ problem_cookbook = if top_level
39
+ 'the inspec.yml for this profile.'
40
+ else
41
+ "the dependency information for #{path_string.split(' ').last}"
42
+ end
43
+ fail Inspec::DuplicateDep, "The dependency #{dep.name} is listed twice in #{problem_cookbook}"
44
+ else
45
+ seen_items_local << dep.name
46
+ end
47
+ end
48
+ end
49
+
50
+ def resolve(deps, top_level = true, seen_items = {}, path_string = '') # rubocop:disable Metrics/AbcSize
35
51
  graph = {}
36
52
  if top_level
37
53
  Inspec::Log.debug("Starting traversal of dependencies #{deps.map(&:name)}")
@@ -39,28 +55,29 @@ module Inspec
39
55
  Inspec::Log.debug("Traversing dependency tree of transitive dependency #{deps.map(&:name)}")
40
56
  end
41
57
 
58
+ detect_duplicates(deps, top_level, path_string)
42
59
  deps.each do |dep|
43
- path_string = if path_string.empty?
44
- dep.name
45
- else
46
- path_string + " -> #{dep.name}"
47
- end
60
+ new_seen_items = seen_items.dup
61
+ new_path_string = if path_string.empty?
62
+ dep.name
63
+ else
64
+ path_string + " -> #{dep.name}"
65
+ end
48
66
 
49
- if seen_items.key?(dep.source_url)
50
- fail Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{path_string})"
67
+ if new_seen_items.key?(dep.resolved_source)
68
+ fail Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{new_path_string})"
51
69
  else
52
- seen_items[dep.source_url] = true
70
+ new_seen_items[dep.resolved_source] = true
53
71
  end
54
72
 
55
73
  if !dep.source_satisfies_spec?
56
- fail Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.source_url} has a version #{dep.source_version} which doesn't match #{dep.required_version}"
74
+ fail Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.resolved_source} has a version #{dep.source_version} which doesn't match #{dep.required_version}"
57
75
  end
58
76
 
59
- Inspec::Log.debug("Adding #{dep.source_url}")
77
+ Inspec::Log.debug("Adding dependency #{dep.name} (#{dep.resolved_source})")
60
78
  graph[dep.name] = dep
61
79
  if !dep.dependencies.empty?
62
- # Recursively resolve any transitive dependencies.
63
- resolve(dep.dependencies, false, seen_items.dup, path_string)
80
+ resolve(dep.dependencies, false, new_seen_items.dup, new_path_string)
64
81
  end
65
82
  end
66
83
 
@@ -23,32 +23,17 @@ module Inspec
23
23
  FileUtils.mkdir_p(@path) unless File.directory?(@path)
24
24
  end
25
25
 
26
- def add(name, source, path_from)
27
- path_to = base_path_for(name, source)
28
- path_to = if File.directory?(path_to)
29
- path_to
30
- elsif path_from.end_with?('.zip')
31
- "#{path_to}.tar.gz"
32
- elsif path_from.end_with?('.tar.gz')
33
- "#{path_to}.tar.gz"
34
- else
35
- fail "Cannot add unknown archive #{path} to vendor index"
36
- end
37
- FileUtils.cp_r(path_from, path_to)
38
- path_to
39
- end
40
-
41
- def prefered_entry_for(name, source_url)
42
- path = base_path_for(name, source_url)
26
+ def prefered_entry_for(key)
27
+ path = base_path_for(key)
43
28
  if File.directory?(path)
44
29
  path
45
30
  else
46
- archive_entry_for(name, source_url)
31
+ archive_entry_for(key)
47
32
  end
48
33
  end
49
34
 
50
- def archive_entry_for(name, source_url)
51
- path = base_path_for(name, source_url)
35
+ def archive_entry_for(key)
36
+ path = base_path_for(key)
52
37
  if File.exist?("#{path}.tar.gz")
53
38
  "#{path}.tar.gz"
54
39
  elsif File.exist?("#{path}.zip")
@@ -64,8 +49,8 @@ module Inspec
64
49
  # @param [String] source_url
65
50
  # @return [Boolean]
66
51
  #
67
- def exists?(name, source_url)
68
- path = base_path_for(name, source_url)
52
+ def exists?(key)
53
+ path = base_path_for(key)
69
54
  File.directory?(path) || File.exist?("#{path}.tar.gz") || File.exist?("#{path}.zip")
70
55
  end
71
56
 
@@ -80,26 +65,8 @@ module Inspec
80
65
  # @param [String] source_url
81
66
  # @return [String]
82
67
  #
83
- def base_path_for(name, source_url)
84
- File.join(@path, key_for(name, source_url))
85
- end
86
-
87
- private
88
-
89
- #
90
- # Return the key for a given profile in the vendor index.
91
- #
92
- # The `source_url` parameter should be a URI-like string that
93
- # fully specifies the source of the exact version we want to pull
94
- # down.
95
- #
96
- # @param [String] name
97
- # @param [String] source_url
98
- # @return [String]
99
- #
100
- def key_for(name, source_url)
101
- source_hash = Digest::SHA256.hexdigest source_url
102
- "#{name}-#{source_hash}"
68
+ def base_path_for(cache_key)
69
+ File.join(@path, cache_key)
103
70
  end
104
71
  end
105
72
  end
data/lib/inspec/errors.rb CHANGED
@@ -8,4 +8,5 @@ module Inspec
8
8
  # dependency resolution
9
9
  class CyclicDependencyError < Error; end
10
10
  class UnsatisfiedVersionSpecification < Error; end
11
+ class DuplicateDep < Error; end
11
12
  end
@@ -17,6 +17,5 @@ module Inspec
17
17
  end
18
18
 
19
19
  require 'fetchers/local'
20
- require 'fetchers/zip'
21
- require 'fetchers/tar'
22
20
  require 'fetchers/url'
21
+ require 'fetchers/git'