inspec/lib/fetchers/url.rb
Adam Leff 8269d0da9e Support vendored profiles in Habitat-packaged profiles
This change adds support in Habitat-packaged profiles for
profiles that depend on other profiles. When `inspec habitat
profile create` or `inspec habitat profile upload` is run,
it will see if the profile's dependencies have been vendored
yet, and if not, it will vendor them before creating the
habitat artifact.

For the git and URL fetchers, more explicit creation of the
target directories for the vendored profiles is done. This
is implicitly done via normal CLI interactions a user may
go through, but in our case, we want to ensure those directories
are there before the fetchers try to write out content.

By adding this support, we also fix a bug experienced in Habitat
where a profile that was packaged before an `inspec exec` was run
for the profile would cause a failure in Habitat. This is caused
by `inspec exec` doing a vendor of the dependencies if necessary
and generating the inspec.lock file. In Habitat, the package dir
is not writable by the hab user and InSpec would fail to run due
to an inability to write out an inspec.lock.

Signed-off-by: Adam Leff <adam@leff.co>
2017-03-29 10:22:20 +02:00

164 lines
5.2 KiB
Ruby

# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann
require 'uri'
require 'digest'
require 'tempfile'
require 'open-uri'
module Fetchers
class Url < Inspec.fetcher(1) # rubocop:disable Metrics/ClassLength
MIME_TYPES = {
'application/x-zip-compressed' => '.zip',
'application/zip' => '.zip',
'application/x-gzip' => '.tar.gz',
'application/gzip' => '.tar.gz',
}.freeze
name 'url'
priority 200
def self.resolve(target, opts = {})
if target.is_a?(Hash) && target.key?(:url)
resolve_from_string(target[:url], opts)
elsif target.is_a?(String)
resolve_from_string(target, opts)
end
end
def self.resolve_from_string(target, opts)
uri = URI.parse(target)
return nil if uri.nil? or uri.scheme.nil?
return nil unless %{ http https }.include? uri.scheme
target = transform(target)
new(target, opts)
rescue URI::Error
nil
end
# Transforms a browser github url to github tar url
# We distinguish between three different Github URL types:
# - Master URL
# - Branch URL
# - Commit URL
#
# master url:
# https://github.com/nathenharvey/tmp_compliance_profile/ is transformed to
# https://github.com/nathenharvey/tmp_compliance_profile/archive/master.tar.gz
#
# github branch:
# https://github.com/hardening-io/tests-os-hardening/tree/2.0 is transformed to
# https://github.com/hardening-io/tests-os-hardening/archive/2.0.tar.gz
#
# github commit:
# https://github.com/hardening-io/tests-os-hardening/tree/48bd4388ddffde68badd83aefa654e7af3231876
# is transformed to
# https://github.com/hardening-io/tests-os-hardening/archive/48bd4388ddffde68badd83aefa654e7af3231876.tar.gz
GITHUB_URL_REGEX = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)(\.git)?(/)?$}
GITHUB_URL_WITH_TREE_REGEX = %r{^https?://(www\.)?github\.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/tree/(?<commit>[\w\.]+)(/)?$}
def self.transform(target)
transformed_target = if m = GITHUB_URL_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
"https://github.com/#{m[:user]}/#{m[:repo]}/archive/master.tar.gz"
elsif m = GITHUB_URL_WITH_TREE_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
"https://github.com/#{m[:user]}/#{m[:repo]}/archive/#{m[:commit]}.tar.gz"
end
if transformed_target
Inspec::Log.warn("URL target #{target} transformed to #{transformed_target}. Consider using the git fetcher")
transformed_target
else
target
end
end
attr_reader :files, :archive_path
def initialize(url, opts)
@target = url
@insecure = opts['insecure']
@token = opts['token']
@config = opts
end
def fetch(path)
@archive_path ||= download_archive(path)
end
def resolved_source
@resolved_source ||= { url: @target, sha256: sha256 }
end
def cache_key
@archive_shasum ||= sha256
end
def to_s
@target
end
private
def sha256
file = @archive_path || temp_archive_path
Digest::SHA256.hexdigest File.read(file)
end
def file_type_from_remote(remote)
content_type = remote.meta['content-type']
file_type = MIME_TYPES[content_type]
if file_type.nil?
Inspec::Log.warn("Unrecognized content type: #{content_type}. Assuming tar.gz")
file_type = '.tar.gz'
end
file_type
end
def temp_archive_path
@temp_archive_path ||= download_archive_to_temp
end
# Downloads archive to temporary file with side effect :( of setting @archive_type
def download_archive_to_temp
return @temp_archive_path if ! @temp_archive_path.nil?
Inspec::Log.debug("Fetching URL: #{@target}")
http_opts = {}
http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if @insecure
if @config
if @config['server_type'] == 'automate'
http_opts['chef-delivery-enterprise'] = @config['automate']['ent']
if @config['automate']['token_type'] == 'dctoken'
http_opts['x-data-collector-token'] = @config['token']
else
http_opts['chef-delivery-user'] = @config['user']
http_opts['chef-delivery-token'] = @config['token']
end
elsif @token
http_opts['Authorization'] = "Bearer #{@token}"
end
end
remote = open(@target, http_opts)
@archive_type = file_type_from_remote(remote) # side effect :(
archive = Tempfile.new(['inspec-dl-', @archive_type])
archive.binmode
archive.write(remote.read)
archive.rewind
archive.close
Inspec::Log.debug("Archive stored at temporary location: #{archive.path}")
@temp_archive_path = archive.path
end
def download_archive(path)
download_archive_to_temp
final_path = "#{path}#{@archive_type}"
FileUtils.mkdir_p(File.dirname(final_path))
FileUtils.mv(temp_archive_path, final_path)
FileUtils.chmod(0644, final_path)
Inspec::Log.debug("Fetched archive moved to: #{final_path}")
@temp_archive_path = nil
final_path
end
end
end