abide_dev_utils 0.6.0 → 0.9.3

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.
@@ -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
@@ -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
- utils = AbideDevUtils::XCCDF::UtilsObject
84
- parsed_xccdf = utils.parse(xccdf)
85
- return add_cis_comment_to_all(path, parsed_xccdf, utils, number_format: number_format) if File.directory?(path)
86
- return add_cis_comment_to_single(path, parsed_xccdf, utils, number_format: number_format) if File.file?(path)
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, utils, number_format: false)
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
- utils.all_cis_recommendations(xccdf),
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, utils, number_format: false)
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, recommendations, number_format, utils)
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
- next if line.match?(/#{comment}/)
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, recommendations, number_format, utils)
140
- reco_text = utils.find_cis_recommendation(
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 reco_text.nil?
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
- "# #{reco_text}"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbideDevUtils
4
- VERSION = "0.6.0"
4
+ VERSION = "0.9.3"
5
5
  end