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