abide_dev_utils 0.1.0
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.
- 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
|