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.
@@ -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