abide_dev_utils 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +136 -0
- data/.rubocop_todo.yml +49 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +12 -0
- data/abide_dev_utils.gemspec +53 -0
- data/bin/abide.rb +6 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/abide +6 -0
- data/lib/abide_dev_utils.rb +9 -0
- data/lib/abide_dev_utils/cli.rb +35 -0
- data/lib/abide_dev_utils/cli/jira.rb +113 -0
- data/lib/abide_dev_utils/cli/puppet.rb +58 -0
- data/lib/abide_dev_utils/cli/test.rb +84 -0
- data/lib/abide_dev_utils/cli/xccdf.rb +48 -0
- data/lib/abide_dev_utils/config.rb +24 -0
- data/lib/abide_dev_utils/constants.rb +17 -0
- data/lib/abide_dev_utils/errors.rb +5 -0
- data/lib/abide_dev_utils/errors/base.rb +27 -0
- data/lib/abide_dev_utils/errors/general.rb +52 -0
- data/lib/abide_dev_utils/errors/jira.rb +21 -0
- data/lib/abide_dev_utils/errors/xccdf.rb +12 -0
- data/lib/abide_dev_utils/files.rb +47 -0
- data/lib/abide_dev_utils/jira.rb +181 -0
- data/lib/abide_dev_utils/output.rb +44 -0
- data/lib/abide_dev_utils/ppt.rb +135 -0
- data/lib/abide_dev_utils/prompt.rb +32 -0
- data/lib/abide_dev_utils/utils/general.rb +9 -0
- data/lib/abide_dev_utils/validate.rb +31 -0
- data/lib/abide_dev_utils/version.rb +5 -0
- data/lib/abide_dev_utils/xccdf.rb +24 -0
- data/lib/abide_dev_utils/xccdf/cis.rb +3 -0
- data/lib/abide_dev_utils/xccdf/cis/hiera.rb +138 -0
- metadata +266 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'abide_dev_utils/errors/base'
|
4
|
+
|
5
|
+
module AbideDevUtils
|
6
|
+
module Errors
|
7
|
+
# Raised when an xpath search of an xccdf file fails
|
8
|
+
class XPathSearchError < GenericError
|
9
|
+
@default = 'XPath seach failed to find anything at:'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AbideDevUtils
|
4
|
+
module Files
|
5
|
+
class Writer
|
6
|
+
MSG_EXT_APPEND = 'Appending %s extension to file'
|
7
|
+
|
8
|
+
def write(content, file: nil, add_ext: true, file_ext: nil)
|
9
|
+
valid_file = add_ext ? append_ext(file, file_ext) : file
|
10
|
+
File.open(valid_file, 'w') { |f| f.write(content) }
|
11
|
+
verify_write(valid_file)
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(m, *args, **kwargs, &_block)
|
15
|
+
if m.to_s.match?(/^write_/)
|
16
|
+
ext = m.to_s.split('_')[-1]
|
17
|
+
write(args[0], **kwargs, file_ext: ext)
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to_missing?(method_name, include_private = false)
|
24
|
+
method_name.to_s.start_with?('write_') || super
|
25
|
+
end
|
26
|
+
|
27
|
+
def append_ext(file_path, ext)
|
28
|
+
return file_path if ext.nil?
|
29
|
+
|
30
|
+
s_ext = ".#{ext}"
|
31
|
+
unless File.extname(file_path) == s_ext
|
32
|
+
puts MSG_EXT_APPEND % s_ext
|
33
|
+
file_path << s_ext
|
34
|
+
end
|
35
|
+
file_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def verify_write(file_path)
|
39
|
+
if File.file?(file_path)
|
40
|
+
puts "Successfully wrote to #{file_path}"
|
41
|
+
else
|
42
|
+
puts "Something went wrong! Failed writing to #{file_path}!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jira-ruby'
|
4
|
+
require 'abide_dev_utils/output'
|
5
|
+
require 'abide_dev_utils/prompt'
|
6
|
+
require 'abide_dev_utils/config'
|
7
|
+
require 'abide_dev_utils/errors/jira'
|
8
|
+
|
9
|
+
module AbideDevUtils
|
10
|
+
module Jira
|
11
|
+
ERRORS = AbideDevUtils::Errors::Jira
|
12
|
+
COV_PARENT_SUMMARY_PREFIX = '::BENCHMARK:: '
|
13
|
+
COV_CHILD_SUMMARY_PREFIX = '::CONTROL:: '
|
14
|
+
|
15
|
+
def self.project(client, project)
|
16
|
+
client.Project.find(project)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.issue(client, issue)
|
20
|
+
client.Issue.find(issue)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.myself(client)
|
24
|
+
client.User.myself
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.issuetype(client, id)
|
28
|
+
client.Issuetype.find(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.priority(client, id)
|
32
|
+
client.Priority.find(id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.all_project_issues_attrs(project)
|
36
|
+
raw_issues = project.issues
|
37
|
+
raw_issues.collect(&:attrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.new_issue(client, project, summary, dry_run: false)
|
41
|
+
if dry_run
|
42
|
+
sleep(0.2)
|
43
|
+
return Dummy.new
|
44
|
+
end
|
45
|
+
fields = {}
|
46
|
+
fields['summary'] = summary
|
47
|
+
fields['project'] = project(client, project)
|
48
|
+
fields['reporter'] = myself(client)
|
49
|
+
fields['issuetype'] = issuetype(client, '3')
|
50
|
+
fields['priority'] = priority(client, '6')
|
51
|
+
issue = client.Issue.build
|
52
|
+
raise ERRORS::CreateIssueError, issue.attrs unless issue.save({ 'fields' => fields })
|
53
|
+
|
54
|
+
issue
|
55
|
+
end
|
56
|
+
|
57
|
+
# This should probably be threaded in the future
|
58
|
+
def self.bulk_new_issue(client, project, summaries, dry_run: false)
|
59
|
+
summaries.each { |s| new_issue(client, project, s, dry_run: dry_run) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.new_subtask(client, issue, summary, dry_run: false)
|
63
|
+
if dry_run
|
64
|
+
sleep(0.2)
|
65
|
+
return Dummy.new
|
66
|
+
end
|
67
|
+
issue_fields = issue.attrs['fields']
|
68
|
+
fields = {}
|
69
|
+
fields['parent'] = issue
|
70
|
+
fields['summary'] = summary
|
71
|
+
fields['project'] = issue_fields['project']
|
72
|
+
fields['reporter'] = myself(client)
|
73
|
+
fields['issuetype'] = issuetype(client, '5')
|
74
|
+
fields['priority'] = issue_fields['priority']
|
75
|
+
subtask = client.Issue.build
|
76
|
+
raise ERRORS::CreateSubtaskError, subtask.attrs unless subtask.save({ 'fields' => fields })
|
77
|
+
|
78
|
+
subtask
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.bulk_new_subtask(client, issue, summaries, dry_run: false)
|
82
|
+
summaries.each do |s|
|
83
|
+
new_subtask(client, issue, s, dry_run: dry_run)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.client(options: {})
|
88
|
+
opts = merge_options(options)
|
89
|
+
opts[:username] = AbideDevUtils::Prompt.username if opts[:username].nil?
|
90
|
+
opts[:password] = AbideDevUtils::Prompt.password if opts[:password].nil?
|
91
|
+
opts[:site] = AbideDevUtils::Prompt.single_line('Jira URL') if opts[:site].nil?
|
92
|
+
opts[:context_path] = '' if opts[:context_path].nil?
|
93
|
+
opts[:auth_type] = :basic if opts[:auth_type].nil?
|
94
|
+
JIRA::Client.new(opts)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.client_from_prompts(http_debug: false)
|
98
|
+
options = {}
|
99
|
+
options[:username] = AbideDevUtils::Prompt.username
|
100
|
+
options[:password] = AbideDevUtils::Prompt.password
|
101
|
+
options[:site] = AbideDevUtils::Prompt.single_line('Jira URL')
|
102
|
+
options[:context_path] = ''
|
103
|
+
options[:auth_type] = :basic
|
104
|
+
options[:http_debug] = http_debug
|
105
|
+
JIRA::Client.new(options)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.project_from_prompts(http_debug: false)
|
109
|
+
client = client_from_prompts(http_debug)
|
110
|
+
project = AbideDevUtils::Prompt.single_line('Project').upcase
|
111
|
+
client.Project.find(project)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.new_issues_from_coverage(client, project, report, dry_run: false)
|
115
|
+
dr_prefix = dry_run ? 'DRY RUN: ' : ''
|
116
|
+
i_attrs = all_project_issues_attrs(project)
|
117
|
+
rep_sums = summaries_from_coverage_report(report)
|
118
|
+
rep_sums.each do |k, v|
|
119
|
+
next if summary_exist?(k, i_attrs)
|
120
|
+
|
121
|
+
parent = new_issue(client, project.attrs['key'], k.to_s, dry_run: dry_run)
|
122
|
+
AbideDevUtils::Output.simple("#{dr_prefix}Created parent issue #{k}")
|
123
|
+
parent_issue = issue(client, parent.attrs['key']) unless parent.respond_to?(:dummy)
|
124
|
+
AbideDevUtils::Output.simple("#{dr_prefix}Creating subtasks, this can take a while...")
|
125
|
+
progress = AbideDevUtils::Output.progress(title: "#{dr_prefix}Creating Subtasks", total: nil)
|
126
|
+
v.each do |s|
|
127
|
+
next if summary_exist?(s, i_attrs)
|
128
|
+
|
129
|
+
progress.title = "#{dr_prefix}#{s}"
|
130
|
+
new_subtask(client, parent_issue, s, dry_run: dry_run)
|
131
|
+
progress.increment
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.merge_options(options)
|
137
|
+
config.merge(options)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.config
|
141
|
+
AbideDevUtils::Config.config_section(:jira)
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.summary_exist?(summary, issue_attrs)
|
145
|
+
issue_attrs.each do |i|
|
146
|
+
return true if i['fields']['summary'] == summary
|
147
|
+
end
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.summaries_from_coverage_report(report)
|
152
|
+
summaries = {}
|
153
|
+
benchmark = nil
|
154
|
+
report.each do |k, v|
|
155
|
+
benchmark = v if k == 'benchmark'
|
156
|
+
next unless k.match?(/^profile_/)
|
157
|
+
|
158
|
+
parent_sum = k
|
159
|
+
v.each do |sk, sv|
|
160
|
+
next unless sk == 'uncovered'
|
161
|
+
|
162
|
+
summaries[parent_sum] = sv.collect { |s| "#{COV_CHILD_SUMMARY_PREFIX}#{s}" }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
summaries.transform_keys { |k| "#{COV_PARENT_SUMMARY_PREFIX}#{benchmark}-#{k}"}
|
166
|
+
end
|
167
|
+
|
168
|
+
class Dummy
|
169
|
+
def attrs
|
170
|
+
{ 'fields' => {
|
171
|
+
'project' => 'dummy',
|
172
|
+
'priority' => 'dummy'
|
173
|
+
} }
|
174
|
+
end
|
175
|
+
|
176
|
+
def dummy
|
177
|
+
true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'pp'
|
5
|
+
require 'yaml'
|
6
|
+
require 'ruby-progressbar'
|
7
|
+
require 'abide_dev_utils/validate'
|
8
|
+
require 'abide_dev_utils/files'
|
9
|
+
|
10
|
+
module AbideDevUtils
|
11
|
+
module Output
|
12
|
+
FWRITER = AbideDevUtils::Files::Writer.new
|
13
|
+
def self.simple(msg, stream: $stdout)
|
14
|
+
stream.puts msg
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.json(in_obj, console: false, file: nil, pretty: true)
|
18
|
+
AbideDevUtils::Validate.hashable(in_obj)
|
19
|
+
json_out = pretty ? JSON.pretty_generate(in_obj) : JSON.generate(in_obj)
|
20
|
+
simple(json_out) if console
|
21
|
+
FWRITER.write_json(json_out, file: file) unless file.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.yaml(in_obj, console: false, file: nil)
|
25
|
+
AbideDevUtils::Validate.hashable(in_obj)
|
26
|
+
# Use object's #to_yaml method if it exists, convert to hash if not
|
27
|
+
yaml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
|
28
|
+
simple(yaml_out) if console
|
29
|
+
FWRITER.write_yaml(yaml_out, file: file) unless file.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.yml(in_obj, console: false, file: nil)
|
33
|
+
AbideDevUtils::Validate.hashable(in_obj)
|
34
|
+
# Use object's #to_yaml method if it exists, convert to hash if not
|
35
|
+
yml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
|
36
|
+
simple(yml_out) if console
|
37
|
+
FWRITER.write_yml(yml_out, file: file) unless file.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.progress(title: 'Progress', start: 0, total: 100)
|
41
|
+
ProgressBar.create(title: title, starting_at: start, total: total)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'pathname'
|
5
|
+
require 'yaml'
|
6
|
+
require 'puppet_pal'
|
7
|
+
|
8
|
+
module AbideDevUtils
|
9
|
+
module Ppt
|
10
|
+
def self.coverage_report(puppet_class_dir, hiera_path, profile = nil)
|
11
|
+
coverage = {}
|
12
|
+
coverage['classes'] = {}
|
13
|
+
all_cap = find_all_classes_and_paths(puppet_class_dir)
|
14
|
+
invalid_classes = find_invalid_classes(all_cap)
|
15
|
+
valid_classes = all_cap.dup.transpose[0] - invalid_classes
|
16
|
+
coverage['classes']['invalid'] = invalid_classes
|
17
|
+
coverage['classes']['valid'] = valid_classes
|
18
|
+
hiera = YAML.safe_load(File.open(hiera_path))
|
19
|
+
matcher = profile.nil? ? /^profile_/ : /^profile_#{profile}/
|
20
|
+
hiera.each do |k, v|
|
21
|
+
key_base = k.split('::')[-1]
|
22
|
+
coverage['benchmark'] = v if key_base == 'title'
|
23
|
+
next unless key_base.match?(matcher)
|
24
|
+
|
25
|
+
coverage[key_base] = generate_uncovered_data(v, valid_classes)
|
26
|
+
end
|
27
|
+
coverage
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.generate_uncovered_data(ctrl_list, valid_classes)
|
31
|
+
out_hash = {}
|
32
|
+
out_hash[:num_total] = ctrl_list.length
|
33
|
+
out_hash[:uncovered] = []
|
34
|
+
out_hash[:covered] = []
|
35
|
+
ctrl_list.each do |c|
|
36
|
+
if valid_classes.include?(c)
|
37
|
+
out_hash[:covered] << c
|
38
|
+
else
|
39
|
+
out_hash[:uncovered] << c
|
40
|
+
end
|
41
|
+
end
|
42
|
+
out_hash[:num_covered] = out_hash[:covered].length
|
43
|
+
out_hash[:num_uncovered] = out_hash[:uncovered].length
|
44
|
+
out_hash[:coverage] = Float(
|
45
|
+
(Float(out_hash[:num_covered]) / Float(out_hash[:num_total])) * 100.0
|
46
|
+
).floor(3)
|
47
|
+
out_hash
|
48
|
+
end
|
49
|
+
|
50
|
+
# Given a directory holding Puppet manifests, returns
|
51
|
+
# the full namespace for all classes in that directory.
|
52
|
+
# @param puppet_class_dir [String] path to a dir containing Puppet manifests
|
53
|
+
# @return [String] The namespace for all classes in manifests in the dir
|
54
|
+
def self.find_class_namespace(puppet_class_dir)
|
55
|
+
path = Pathname.new(puppet_class_dir)
|
56
|
+
mod_root = nil
|
57
|
+
ns_parts = []
|
58
|
+
found_manifests = false
|
59
|
+
path.ascend do |p|
|
60
|
+
if found_manifests
|
61
|
+
mod_root = find_mod_root(p)
|
62
|
+
break
|
63
|
+
end
|
64
|
+
if File.basename(p) == 'manifests'
|
65
|
+
found_manifests = true
|
66
|
+
next
|
67
|
+
else
|
68
|
+
ns_parts << File.basename(p)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
"#{mod_root}::#{ns_parts.reverse.join('::')}::"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Given a Pathname object of the 'manifests' directory in a Puppet module,
|
75
|
+
# determines the module namespace root. Does this by consulting
|
76
|
+
# metadata.json, if it exists, or by using the parent directory name.
|
77
|
+
# @param pathname [Pathname] A Pathname object of the module's manifests dir
|
78
|
+
# @return [String] The module's namespace root
|
79
|
+
def self.find_mod_root(pathname)
|
80
|
+
metadata_file = nil
|
81
|
+
pathname.entries.each do |e|
|
82
|
+
metadata_file = "#{pathname}/metadata.json" if File.basename(e) == 'metadata.json'
|
83
|
+
end
|
84
|
+
if metadata_file.nil?
|
85
|
+
File.basename(p)
|
86
|
+
else
|
87
|
+
File.open(metadata_file) do |f|
|
88
|
+
file = JSON.parse(f.read)
|
89
|
+
File.basename(p) unless file.key?('name')
|
90
|
+
file['name'].split('-')[-1]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Array] An array of frozen arrays where each sub-array's
|
96
|
+
# index 0 is class_name and index 1 is the full path to the file.
|
97
|
+
def self.find_all_classes_and_paths(puppet_class_dir)
|
98
|
+
all_cap = []
|
99
|
+
Dir.each_child(puppet_class_dir) do |c|
|
100
|
+
path = "#{puppet_class_dir}/#{c}"
|
101
|
+
next if File.directory?(path) || File.extname(path) != '.pp'
|
102
|
+
|
103
|
+
all_cap << [File.basename(path, '.pp'), path].freeze
|
104
|
+
end
|
105
|
+
all_cap
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.find_valid_classes(all_cap)
|
109
|
+
all_classes = all_cap.dup.transpose[0]
|
110
|
+
all_classes - find_invalid_classes(all_cap)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.find_invalid_classes(all_cap)
|
114
|
+
invalid_classes = []
|
115
|
+
all_cap.each do |cap|
|
116
|
+
invalid_classes << cap[0] unless class_valid?(cap[1])
|
117
|
+
end
|
118
|
+
invalid_classes
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.class_valid?(manifest_path)
|
122
|
+
compiler = Puppet::Pal::Compiler.new(nil)
|
123
|
+
ast = compiler.parse_file(manifest_path)
|
124
|
+
ast.body.body.statements.each do |s|
|
125
|
+
next unless s.respond_to?(:arguments)
|
126
|
+
next unless s.arguments.respond_to?(:each)
|
127
|
+
|
128
|
+
s.arguments.each do |i|
|
129
|
+
return false if i.value == 'Not implemented'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
module AbideDevUtils
|
6
|
+
module Prompt
|
7
|
+
def self.yes_no(msg)
|
8
|
+
print "#{msg} (Y/n): "
|
9
|
+
return true if $stdin.cooked(&:gets).match?(/^[Yy].*/)
|
10
|
+
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.single_line(msg)
|
15
|
+
print "#{msg}: "
|
16
|
+
$stdin.cooked(&:gets).chomp
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.username
|
20
|
+
print 'Username: '
|
21
|
+
$stdin.cooked(&:gets).chomp
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.password
|
25
|
+
$stdin.getpass('Password:')
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.secure(msg)
|
29
|
+
$stdin.getpass(msg)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|