inspec 0.33.2 → 0.34.0

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