abide_dev_utils 0.6.0 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/.rubocop.yml +1 -1
- data/CODEOWNERS +1 -0
- data/Gemfile.lock +273 -0
- data/abide_dev_utils.gemspec +7 -6
- data/lib/abide_dev_utils/cli/comply.rb +26 -7
- data/lib/abide_dev_utils/cli/puppet.rb +18 -0
- data/lib/abide_dev_utils/cli/xccdf.rb +77 -11
- data/lib/abide_dev_utils/comply.rb +240 -169
- data/lib/abide_dev_utils/errors/comply.rb +4 -0
- data/lib/abide_dev_utils/errors/general.rb +9 -0
- data/lib/abide_dev_utils/errors/xccdf.rb +12 -0
- data/lib/abide_dev_utils/gcloud.rb +2 -1
- data/lib/abide_dev_utils/output.rb +7 -3
- data/lib/abide_dev_utils/ppt/api.rb +219 -0
- data/lib/abide_dev_utils/ppt/score_module.rb +162 -0
- data/lib/abide_dev_utils/ppt.rb +22 -19
- data/lib/abide_dev_utils/validate.rb +5 -1
- data/lib/abide_dev_utils/version.rb +1 -1
- data/lib/abide_dev_utils/xccdf.rb +627 -11
- metadata +30 -16
- data/.dockerignore +0 -1
- data/Dockerfile +0 -23
- data/lib/abide_dev_utils/xccdf/cis/hiera.rb +0 -166
- data/lib/abide_dev_utils/xccdf/cis.rb +0 -3
- data/lib/abide_dev_utils/xccdf/utils.rb +0 -85
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
require 'json'
|
5
|
+
require 'net/http'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
module AbideDevUtils
|
9
|
+
module Ppt
|
10
|
+
class ApiClient
|
11
|
+
attr_reader :hostname, :custom_ports
|
12
|
+
attr_writer :auth_token, :tls_cert_verify
|
13
|
+
attr_accessor :content_type
|
14
|
+
|
15
|
+
CT_JSON = 'application/json'
|
16
|
+
API_DEFS = {
|
17
|
+
codemanager: {
|
18
|
+
port: 8170,
|
19
|
+
version: 'v1',
|
20
|
+
base: 'code-manager',
|
21
|
+
paths: [
|
22
|
+
{
|
23
|
+
path: 'deploys',
|
24
|
+
verbs: %w[post],
|
25
|
+
x_auth: true
|
26
|
+
}
|
27
|
+
]
|
28
|
+
},
|
29
|
+
classifier1: {
|
30
|
+
port: 4433,
|
31
|
+
version: 'v1',
|
32
|
+
base: 'classifier-api',
|
33
|
+
paths: [
|
34
|
+
{
|
35
|
+
path: 'groups',
|
36
|
+
verbs: %w[get post],
|
37
|
+
x_auth: true
|
38
|
+
}
|
39
|
+
]
|
40
|
+
},
|
41
|
+
orchestrator: {
|
42
|
+
port: 8143,
|
43
|
+
version: 'v1',
|
44
|
+
base: 'orchestrator',
|
45
|
+
paths: [
|
46
|
+
{
|
47
|
+
path: 'command/deploy',
|
48
|
+
verbs: %w[post],
|
49
|
+
x_auth: true
|
50
|
+
},
|
51
|
+
{
|
52
|
+
path: 'command/task',
|
53
|
+
verbs: %w[post],
|
54
|
+
x_auth: true
|
55
|
+
},
|
56
|
+
{
|
57
|
+
path: 'jobs',
|
58
|
+
verbs: %w[get],
|
59
|
+
x_auth: true
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
def initialize(hostname, auth_token: nil, content_type: CT_JSON, custom_ports: {}, verbose: false)
|
66
|
+
@hostname = hostname
|
67
|
+
@auth_token = auth_token
|
68
|
+
@content_type = content_type
|
69
|
+
@custom_ports = custom_ports
|
70
|
+
@verbose = verbose
|
71
|
+
define_api_methods
|
72
|
+
end
|
73
|
+
|
74
|
+
def login(username, password: nil, lifetime: '1h', label: nil)
|
75
|
+
label = "AbideDevUtils token for #{username} - lifetime #{lifetime}" if label.nil?
|
76
|
+
password = IO.console.getpass 'Password: ' if password.nil?
|
77
|
+
data = {
|
78
|
+
'login' => username,
|
79
|
+
'password' => password,
|
80
|
+
'lifetime' => lifetime,
|
81
|
+
'label' => label
|
82
|
+
}
|
83
|
+
uri = URI("https://#{@hostname}:4433/rbac-api/v1/auth/token")
|
84
|
+
result = http_request(uri, post_request(uri, x_auth: false, **data), json_out: true)
|
85
|
+
@auth_token = result['token']
|
86
|
+
log_verbose("Successfully logged in? #{auth_token?}")
|
87
|
+
auth_token?
|
88
|
+
end
|
89
|
+
|
90
|
+
def auth_token?
|
91
|
+
defined?(@auth_token) && !@auth_token.nil? && !@auth_token.empty?
|
92
|
+
end
|
93
|
+
|
94
|
+
def tls_cert_verify
|
95
|
+
@tls_cert_verify = defined?(@tls_cert_verify) ? @tls_cert_verify : false
|
96
|
+
end
|
97
|
+
|
98
|
+
def verbose?
|
99
|
+
@verbose
|
100
|
+
end
|
101
|
+
|
102
|
+
def no_verbose
|
103
|
+
@verbose = false
|
104
|
+
end
|
105
|
+
|
106
|
+
def verbose!
|
107
|
+
@verbose = true
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def define_api_methods
|
113
|
+
api_method_data.each do |meth, data|
|
114
|
+
case meth
|
115
|
+
when /^get_.*/
|
116
|
+
self.class.define_method(meth) do |*args, **kwargs|
|
117
|
+
uri = args.empty? ? data[:uri] : URI("#{data[:uri]}/#{args.join('/')}")
|
118
|
+
req = get_request(uri, x_auth: data[:x_auth], **kwargs)
|
119
|
+
http_request(data[:uri], req, json_out: true)
|
120
|
+
end
|
121
|
+
when /^post_.*/
|
122
|
+
self.class.define_method(meth) do |*args, **kwargs|
|
123
|
+
uri = args.empty? ? data[:uri] : URI("#{data[:uri]}/#{args.join('/')}")
|
124
|
+
req = post_request(uri, x_auth: data[:x_auth], **kwargs)
|
125
|
+
http_request(data[:uri], req, json_out: true)
|
126
|
+
end
|
127
|
+
else
|
128
|
+
raise "Cannot define method for #{meth}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def api_method_data
|
134
|
+
method_data = {}
|
135
|
+
API_DEFS.each do |key, val|
|
136
|
+
val[:paths].each do |path|
|
137
|
+
method_names = api_method_names(key, path)
|
138
|
+
method_names.each do |name|
|
139
|
+
method_data[name] = {
|
140
|
+
uri: api_method_uri(val[:port], val[:base], val[:version], path[:path]),
|
141
|
+
x_auth: path[:x_auth]
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
method_data
|
147
|
+
end
|
148
|
+
|
149
|
+
def api_method_names(api_name, path)
|
150
|
+
path[:verbs].each_with_object([]) do |verb, ary|
|
151
|
+
path_str = path[:path].split('/').join('_')
|
152
|
+
ary << [verb, api_name.to_s, path_str].join('_')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def api_method_uri(port, base, version, path)
|
157
|
+
URI("https://#{@hostname}:#{port}/#{base}/#{version}/#{path}")
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_request(uri, x_auth: true, **qparams)
|
161
|
+
log_verbose('New GET request:')
|
162
|
+
log_verbose("request_qparams?: #{!qparams.empty?}")
|
163
|
+
uri.query = URI.encode_www_form(qparams) unless qparams.empty?
|
164
|
+
headers = init_headers(x_auth: x_auth)
|
165
|
+
log_verbose("request_headers: #{redact_headers(headers)}")
|
166
|
+
Net::HTTP::Get.new(uri, headers)
|
167
|
+
end
|
168
|
+
|
169
|
+
def post_request(uri, x_auth: true, **data)
|
170
|
+
log_verbose('New POST request:')
|
171
|
+
log_verbose("request_data?: #{!data.empty?}")
|
172
|
+
headers = init_headers(x_auth: x_auth)
|
173
|
+
log_verbose("request_headers: #{redact_headers(headers)}")
|
174
|
+
req = Net::HTTP::Post.new(uri, headers)
|
175
|
+
req.body = data.to_json unless data.empty?
|
176
|
+
req
|
177
|
+
end
|
178
|
+
|
179
|
+
def init_headers(x_auth: true)
|
180
|
+
headers = { 'Content-Type' => @content_type }
|
181
|
+
return headers unless x_auth
|
182
|
+
|
183
|
+
raise 'Auth token not set!' unless auth_token?
|
184
|
+
|
185
|
+
headers['X-Authentication'] = @auth_token
|
186
|
+
headers
|
187
|
+
end
|
188
|
+
|
189
|
+
def http_request(uri, req, json_out: true)
|
190
|
+
result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, verify_mode: tls_verify_mode) do |http|
|
191
|
+
log_verbose("use_ssl: true, verify_mode: #{tls_verify_mode}")
|
192
|
+
http.request(req)
|
193
|
+
end
|
194
|
+
case result.code
|
195
|
+
when '200', '201', '202'
|
196
|
+
json_out ? JSON.parse(result.body) : result
|
197
|
+
else
|
198
|
+
jbody = JSON.parse(result.body)
|
199
|
+
log_verbose("HTTP #{result.code} #{jbody['kind']} #{jbody['msg']} #{jbody['details']} #{uri}")
|
200
|
+
raise "HTTP #{result.code} #{jbody['kind']} #{jbody['msg']} #{jbody['details']} #{uri}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def log_verbose(msg)
|
205
|
+
puts msg if @verbose
|
206
|
+
end
|
207
|
+
|
208
|
+
def redact_headers(headers)
|
209
|
+
r_headers = headers.dup
|
210
|
+
r_headers['X-Authentication'] = 'XXXXX' if r_headers.key?('X-Authentication')
|
211
|
+
r_headers
|
212
|
+
end
|
213
|
+
|
214
|
+
def tls_verify_mode
|
215
|
+
tls_cert_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'metadata-json-lint'
|
5
|
+
require 'puppet-lint'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module AbideDevUtils
|
9
|
+
module Ppt
|
10
|
+
class ScoreModule
|
11
|
+
attr_reader :module_name, :module_dir, :manifests_dir
|
12
|
+
|
13
|
+
def initialize(module_dir)
|
14
|
+
@module_name = module_dir.split(File::SEPARATOR)[-1]
|
15
|
+
@module_dir = real_module_dir(module_dir)
|
16
|
+
@manifests_dir = File.join(real_module_dir(module_dir), 'manifests')
|
17
|
+
@metadata = JSON.parse(File.join(@module_dir, 'metadata.json'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def lint
|
21
|
+
linter_exit_code, linter_output = lint_manifests
|
22
|
+
{
|
23
|
+
exit_code: linter_exit_code,
|
24
|
+
manifests: manifest_count,
|
25
|
+
lines: line_count,
|
26
|
+
linter_version: linter_version,
|
27
|
+
output: linter_output
|
28
|
+
}.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
# def metadata
|
32
|
+
|
33
|
+
# end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def manifests
|
38
|
+
@manifests ||= Dir["#{manifests_dir}/**/*.pp"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def manifest_count
|
42
|
+
@manifest_count ||= manifests.count
|
43
|
+
end
|
44
|
+
|
45
|
+
def line_count
|
46
|
+
@line_count ||= manifests.each_with_object([]) { |x, ary| ary << File.readlines(x).size }.sum
|
47
|
+
end
|
48
|
+
|
49
|
+
def lint_manifests
|
50
|
+
results = []
|
51
|
+
PuppetLint.configuration.with_filename = true
|
52
|
+
PuppetLint.configuration.json = true
|
53
|
+
PuppetLint.configuration.relative = true
|
54
|
+
linter_exit_code = 0
|
55
|
+
manifests.each do |manifest|
|
56
|
+
next if PuppetLint.configuration.ignore_paths.any? { |p| File.fnmatch(p, manifest) }
|
57
|
+
|
58
|
+
linter = PuppetLint.new
|
59
|
+
linter.file = manifest
|
60
|
+
linter.run
|
61
|
+
linter_exit_code = 1 if linter.errors? || linter.warnings?
|
62
|
+
results << linter.problems.reject { |x| x[:kind] == :ignored }
|
63
|
+
end
|
64
|
+
[linter_exit_code, JSON.generate(results)]
|
65
|
+
end
|
66
|
+
|
67
|
+
def lint_metadata
|
68
|
+
results = { errors: [], warnings: [] }
|
69
|
+
results[:errors] << metadata_schema_errors
|
70
|
+
dep_errors, dep_warnings = metadata_validate_deps
|
71
|
+
results[:errors] << dep_errors
|
72
|
+
results[:warnings] << dep_warnings
|
73
|
+
results[:errors] << metadata_deprecated_fields
|
74
|
+
end
|
75
|
+
|
76
|
+
def metadata_schema_errors
|
77
|
+
MetadataJsonLint::Schema.new.validate(@metadata).each_with_object([]) do |err, ary|
|
78
|
+
check = err[:field] == 'root' ? :required_fields : err[:field]
|
79
|
+
ary << metadata_err(check, err[:message])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def metadata_validate_deps
|
84
|
+
return [[], []] unless @metadata.key?('dependencies')
|
85
|
+
|
86
|
+
errors, warnings = []
|
87
|
+
duplicates = metadata_dep_duplicates
|
88
|
+
warnings << duplicates unless duplicates.empty?
|
89
|
+
@metadata['dependencies'].each do |dep|
|
90
|
+
e, w = metadata_dep_version_requirement(dep)
|
91
|
+
errors << e unless e.nil?
|
92
|
+
warnings << w unless w.nil?
|
93
|
+
warnings << metadata_dep_version_range(dep['name']) if dep.key?('version_range')
|
94
|
+
end
|
95
|
+
[errors.flatten, warnings.flatten]
|
96
|
+
end
|
97
|
+
|
98
|
+
def metadata_deprecated_fields
|
99
|
+
%w[types checksum].each_with_object([]) do |field, ary|
|
100
|
+
next unless @metadata.key?(field)
|
101
|
+
|
102
|
+
ary << metadata_err(:deprecated_fields, "Deprecated field '#{field}' found in metadata.json")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def metadata_dep_duplicates
|
107
|
+
results = []
|
108
|
+
duplicates = @metadata['dependencies'].detect { |x| @metadata['dependencies'].count(x) > 1 }
|
109
|
+
return results if duplicates.empty?
|
110
|
+
|
111
|
+
duplicates.each { |x| results << metadata_err(:dependencies, "Duplicate dependencies on #{x}") }
|
112
|
+
results
|
113
|
+
end
|
114
|
+
|
115
|
+
def metadata_dep_version_requirement(dependency)
|
116
|
+
unless dependency.key?('version_requirement')
|
117
|
+
return [metadata_err(:dependencies, "Invalid 'version_requirement' field in metadata.json: #{e}"), nil]
|
118
|
+
end
|
119
|
+
|
120
|
+
ver_req = MetadataJsonLint::VersionRequirement.new(dependency['version_requirement'])
|
121
|
+
return [nil, metadata_dep_open_ended(dependency['name'], dependency['version_requirement'])] if ver_req.open_ended?
|
122
|
+
return [nil, metadata_dep_mixed_syntax(dependency['name'], dependency['version_requirement'])] if ver_req.mixed_syntax?
|
123
|
+
|
124
|
+
[nil, nil]
|
125
|
+
end
|
126
|
+
|
127
|
+
def metadata_dep_open_ended(name, version_req)
|
128
|
+
metadata_err(:dependencies, "Dependency #{name} has an open ended dependency version requirement #{version_req}")
|
129
|
+
end
|
130
|
+
|
131
|
+
def metadata_dep_mixed_syntax(name, version_req)
|
132
|
+
msg = 'Mixing "x" or "*" version syntax with operators is not recommended in ' \
|
133
|
+
"metadata.json, use one style in the #{name} dependency: #{version_req}"
|
134
|
+
metadata_err(:dependencies, msg)
|
135
|
+
end
|
136
|
+
|
137
|
+
def metadata_dep_version_range(name)
|
138
|
+
metadata_err(:dependencies, "Dependency #{name} has a 'version_range' attribute which is no longer used by the forge.")
|
139
|
+
end
|
140
|
+
|
141
|
+
def metadata_err(check, msg)
|
142
|
+
{ check: check, msg: msg }
|
143
|
+
end
|
144
|
+
|
145
|
+
def linter_version
|
146
|
+
PuppetLint::VERSION
|
147
|
+
end
|
148
|
+
|
149
|
+
def relative_manifests
|
150
|
+
Dir.glob('manifests/**/*.pp')
|
151
|
+
end
|
152
|
+
|
153
|
+
def real_module_dir(path)
|
154
|
+
return Pathname.pwd if path.nil?
|
155
|
+
|
156
|
+
return Pathname.new(path).cleanpath(consider_symlink: true) if Dir.exist?(path)
|
157
|
+
|
158
|
+
raise ArgumentError, "Path #{path} is not a directory"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/abide_dev_utils/ppt.rb
CHANGED
@@ -80,31 +80,29 @@ module AbideDevUtils
|
|
80
80
|
|
81
81
|
def self.add_cis_comment(path, xccdf, number_format: false)
|
82
82
|
require 'abide_dev_utils/xccdf'
|
83
|
-
|
84
|
-
parsed_xccdf =
|
85
|
-
return add_cis_comment_to_all(path, parsed_xccdf,
|
86
|
-
return add_cis_comment_to_single(path, parsed_xccdf,
|
83
|
+
|
84
|
+
parsed_xccdf = AbideDevUtils::XCCDF::Benchmark.new(xccdf)
|
85
|
+
return add_cis_comment_to_all(path, parsed_xccdf, number_format: number_format) if File.directory?(path)
|
86
|
+
return add_cis_comment_to_single(path, parsed_xccdf, number_format: number_format) if File.file?(path)
|
87
87
|
|
88
88
|
raise AbideDevUtils::Errors::FileNotFoundError, path
|
89
89
|
end
|
90
90
|
|
91
|
-
def self.add_cis_comment_to_single(path, xccdf,
|
91
|
+
def self.add_cis_comment_to_single(path, xccdf, number_format: false)
|
92
92
|
write_cis_comment_to_file(
|
93
93
|
path,
|
94
94
|
cis_recommendation_comment(
|
95
95
|
path,
|
96
|
-
|
97
|
-
number_format
|
98
|
-
utils
|
96
|
+
xccdf,
|
97
|
+
number_format
|
99
98
|
)
|
100
99
|
)
|
101
100
|
end
|
102
101
|
|
103
|
-
def self.add_cis_comment_to_all(path, xccdf,
|
102
|
+
def self.add_cis_comment_to_all(path, xccdf, number_format: false)
|
104
103
|
comments = {}
|
105
|
-
recommendations = utils.all_cis_recommendations(xccdf)
|
106
104
|
Dir[File.join(path, '*.pp')].each do |puppet_file|
|
107
|
-
comment = cis_recommendation_comment(puppet_file,
|
105
|
+
comment = cis_recommendation_comment(puppet_file, xccdf, number_format)
|
108
106
|
comments[puppet_file] = comment unless comment.nil?
|
109
107
|
end
|
110
108
|
comments.each do |key, value|
|
@@ -120,9 +118,7 @@ module AbideDevUtils
|
|
120
118
|
File.open(tempfile, 'w') do |nf|
|
121
119
|
nf.write("#{comment}\n")
|
122
120
|
File.foreach(path) do |line|
|
123
|
-
|
124
|
-
|
125
|
-
nf << line
|
121
|
+
nf.write(line) unless line == "#{comment}\n"
|
126
122
|
end
|
127
123
|
end
|
128
124
|
File.rename(path, "#{path}.old")
|
@@ -136,17 +132,24 @@ module AbideDevUtils
|
|
136
132
|
end
|
137
133
|
end
|
138
134
|
|
139
|
-
def self.cis_recommendation_comment(puppet_file,
|
140
|
-
|
135
|
+
def self.cis_recommendation_comment(puppet_file, xccdf, number_format)
|
136
|
+
_, control = xccdf.find_cis_recommendation(
|
141
137
|
File.basename(puppet_file, '.pp'),
|
142
|
-
recommendations,
|
143
138
|
number_format: number_format
|
144
139
|
)
|
145
|
-
if
|
140
|
+
if control.nil?
|
146
141
|
AbideDevUtils::Output.simple("Could not find recommendation text for #{puppet_file}...")
|
147
142
|
return nil
|
148
143
|
end
|
149
|
-
|
144
|
+
control_title = xccdf.resolve_control_reference(control).xpath('./xccdf:title').text
|
145
|
+
"# #{control_title}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.score_module(module_path, outfile: nil, quiet: false, checks: ['all'], **_)
|
149
|
+
AbideDevUtils::Output.simple 'This command is not currently implemented'
|
150
|
+
# require 'abide_dev_utils/ppt/score_module'
|
151
|
+
# score = {}
|
152
|
+
# score[:lint_check] = ScoreModule.lint if checks.include?('all') || checks.include?('lint')
|
150
153
|
end
|
151
154
|
end
|
152
155
|
end
|
@@ -8,9 +8,13 @@ module AbideDevUtils
|
|
8
8
|
raise AbideDevUtils::Errors::FileNotFoundError, path unless File.exist?(path)
|
9
9
|
end
|
10
10
|
|
11
|
-
def self.file(path)
|
11
|
+
def self.file(path, extension: nil)
|
12
12
|
filesystem_path(path)
|
13
13
|
raise AbideDevUtils::Errors::PathNotFileError, path unless File.file?(path)
|
14
|
+
return if extension.nil?
|
15
|
+
|
16
|
+
file_ext = extension.match?(/^\.[A-Za-z0-9]+$/) ? extension : ".#{extension}"
|
17
|
+
raise AbideDevUtils::Errors::FileExtensionIncorrectError, extension unless File.extname(path) == file_ext
|
14
18
|
end
|
15
19
|
|
16
20
|
def self.directory(path)
|