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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -2
- data/README.md +12 -13
- data/examples/meta-profile/README.md +11 -0
- data/examples/meta-profile/controls/example.rb +8 -0
- data/examples/meta-profile/inspec.yml +19 -0
- data/lib/bundles/inspec-compliance/target.rb +25 -8
- data/lib/bundles/inspec-supermarket/api.rb +12 -16
- data/lib/bundles/inspec-supermarket/target.rb +11 -6
- data/lib/fetchers/git.rb +162 -0
- data/lib/fetchers/local.rb +33 -16
- data/lib/fetchers/mock.rb +8 -4
- data/lib/fetchers/url.rb +56 -42
- data/lib/inspec/dependencies/lockfile.rb +28 -2
- data/lib/inspec/dependencies/requirement.rb +20 -51
- data/lib/inspec/dependencies/resolver.rb +30 -13
- data/lib/inspec/dependencies/vendor_index.rb +9 -42
- data/lib/inspec/errors.rb +1 -0
- data/lib/inspec/fetcher.rb +1 -2
- data/lib/inspec/file_provider.rb +219 -0
- data/lib/inspec/plugins/fetcher.rb +59 -80
- data/lib/inspec/profile.rb +31 -18
- data/lib/inspec/resource.rb +2 -1
- data/lib/inspec/source_reader.rb +0 -3
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/security_policy.rb +67 -38
- data/lib/resources/sys_info.rb +26 -0
- data/lib/resources/{user.rb → users.rb} +237 -76
- metadata +9 -5
- data/lib/fetchers/tar.rb +0 -53
- data/lib/fetchers/zip.rb +0 -49
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
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
96
|
+
def download_archive(path)
|
70
97
|
http_opts = {}
|
71
|
-
|
72
|
-
http_opts['
|
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
|
-
|
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
|
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[
|
24
|
-
entry[
|
23
|
+
req = new(entry[:name],
|
24
|
+
entry[:version_constraints],
|
25
25
|
vendor_index,
|
26
26
|
cwd,
|
27
|
-
|
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' =>
|
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
|
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(
|
90
|
-
|
91
|
-
|
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(
|
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} (#{
|
135
|
-
end
|
136
|
-
|
137
|
-
def path
|
138
|
-
@path ||= pull
|
106
|
+
"#{dep} (#{resolved_source})"
|
139
107
|
end
|
140
108
|
|
141
109
|
def profile
|
142
|
-
|
143
|
-
opts =
|
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(
|
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
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
50
|
-
fail Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{
|
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
|
-
|
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.
|
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.
|
77
|
+
Inspec::Log.debug("Adding dependency #{dep.name} (#{dep.resolved_source})")
|
60
78
|
graph[dep.name] = dep
|
61
79
|
if !dep.dependencies.empty?
|
62
|
-
|
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
|
27
|
-
|
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(
|
31
|
+
archive_entry_for(key)
|
47
32
|
end
|
48
33
|
end
|
49
34
|
|
50
|
-
def archive_entry_for(
|
51
|
-
path = base_path_for(
|
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?(
|
68
|
-
path = base_path_for(
|
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(
|
84
|
-
File.join(@path,
|
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