firespring_dev_commands 1.3.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
  5. data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
  6. data/lib/firespring_dev_commands/audit/report.rb +49 -0
  7. data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
  8. data/lib/firespring_dev_commands/aws/account.rb +164 -0
  9. data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
  10. data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
  11. data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
  12. data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
  13. data/lib/firespring_dev_commands/aws/login.rb +131 -0
  14. data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
  15. data/lib/firespring_dev_commands/aws/profile.rb +55 -0
  16. data/lib/firespring_dev_commands/aws/s3.rb +42 -0
  17. data/lib/firespring_dev_commands/aws.rb +10 -0
  18. data/lib/firespring_dev_commands/boolean.rb +7 -0
  19. data/lib/firespring_dev_commands/common.rb +112 -0
  20. data/lib/firespring_dev_commands/daterange.rb +171 -0
  21. data/lib/firespring_dev_commands/docker/compose.rb +271 -0
  22. data/lib/firespring_dev_commands/docker/status.rb +38 -0
  23. data/lib/firespring_dev_commands/docker.rb +276 -0
  24. data/lib/firespring_dev_commands/dotenv.rb +6 -0
  25. data/lib/firespring_dev_commands/env.rb +38 -0
  26. data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
  27. data/lib/firespring_dev_commands/eol.rb +58 -0
  28. data/lib/firespring_dev_commands/git/info.rb +13 -0
  29. data/lib/firespring_dev_commands/git.rb +420 -0
  30. data/lib/firespring_dev_commands/jira/issue.rb +33 -0
  31. data/lib/firespring_dev_commands/jira/project.rb +13 -0
  32. data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
  33. data/lib/firespring_dev_commands/jira/user.rb +31 -0
  34. data/lib/firespring_dev_commands/jira.rb +78 -0
  35. data/lib/firespring_dev_commands/logger.rb +8 -0
  36. data/lib/firespring_dev_commands/node/audit.rb +39 -0
  37. data/lib/firespring_dev_commands/node.rb +107 -0
  38. data/lib/firespring_dev_commands/php/audit.rb +71 -0
  39. data/lib/firespring_dev_commands/php.rb +109 -0
  40. data/lib/firespring_dev_commands/rake.rb +24 -0
  41. data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
  42. data/lib/firespring_dev_commands/ruby.rb +113 -0
  43. data/lib/firespring_dev_commands/second.rb +22 -0
  44. data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
  45. data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
  46. data/lib/firespring_dev_commands/tar.rb +149 -0
  47. data/lib/firespring_dev_commands/templates/aws.rb +84 -0
  48. data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
  49. data/lib/firespring_dev_commands/templates/ci.rb +138 -0
  50. data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
  51. data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
  52. data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
  53. data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
  54. data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
  55. data/lib/firespring_dev_commands/templates/eol.rb +23 -0
  56. data/lib/firespring_dev_commands/templates/git.rb +147 -0
  57. data/lib/firespring_dev_commands/version.rb +11 -0
  58. data/lib/firespring_dev_commands.rb +21 -0
  59. metadata +436 -0
@@ -0,0 +1,420 @@
1
+ require 'fileutils'
2
+ require 'git'
3
+
4
+ module Dev
5
+ # Class for performing git functions
6
+ class Git
7
+ # The default base branch to use
8
+ DEFAULT_MAIN_BRANCH = 'master'.freeze
9
+
10
+ # Config object for setting top level git config options
11
+ Config = Struct.new(:main_branch, :staging_branch, :info, :min_version, :max_version) do
12
+ def initialize
13
+ self.main_branch = DEFAULT_MAIN_BRANCH
14
+ self.staging_branch = nil
15
+ self.info = [Dev::Git::Info.new(DEV_COMMANDS_PROJECT_NAME, DEV_COMMANDS_ROOT_DIR)]
16
+ self.min_version = nil
17
+ self.max_version = nil
18
+ end
19
+ end
20
+
21
+ class << self
22
+ # Instantiates a new top level config object if one hasn't already been created
23
+ # Yields that config object to any given block
24
+ # Returns the resulting config object
25
+ def config
26
+ @config ||= Config.new
27
+ yield(@config) if block_given?
28
+ @config
29
+ end
30
+
31
+ # Alias the config method to configure for a slightly clearer access syntax
32
+ alias_method :configure, :config
33
+
34
+ # Returns the version of the git executable running on the system
35
+ def version
36
+ @version ||= ::Git::Lib.new(nil, nil).current_command_version.join('.')
37
+ end
38
+ end
39
+
40
+ attr_accessor :main_branch, :staging_branch, :release_branches, :info, :original_branches
41
+
42
+ def initialize(
43
+ main_branch: self.class.config.main_branch,
44
+ staging_branch: self.class.config.staging_branch,
45
+ info: self.class.config.info
46
+ )
47
+ @main_branch = main_branch
48
+ raise 'main branch must be configured' if main_branch.to_s.empty?
49
+
50
+ @staging_branch = staging_branch || main_branch
51
+ @info = Array(info)
52
+ raise 'git repositories must be configured' if @info.empty? || !@info.all?(Dev::Git::Info)
53
+
54
+ check_version
55
+ end
56
+
57
+ # Checks the min and max version against the current git version if they have been configured
58
+ def check_version
59
+ min_version = self.class.config.min_version
60
+ raise "requires git version >= #{min_version} (found #{self.class.version})" if min_version && !Dev::Common.new.version_greater_than(min_version, self.class.version)
61
+
62
+ max_version = self.class.config.max_version
63
+ raise "requires git version < #{max_version} (found #{self.class.version})" if max_version && Dev::Common.new.version_greater_than(max_version, self.class.version)
64
+ end
65
+
66
+ # Returns all git paths configured in our info
67
+ def project_dirs
68
+ @project_dirs ||= @info.map(&:path).sort
69
+ end
70
+
71
+ # Returns the first configured project dire
72
+ def default_project_dir
73
+ project_dirs.first
74
+ end
75
+
76
+ # Populates and returns a hash containing the original version of branches
77
+ def original_branches
78
+ @original_branches ||= current_branches
79
+ end
80
+
81
+ # Returns a hash of each project repo and the branch that is currently checked out
82
+ def current_branches
83
+ {}.tap do |hsh|
84
+ project_dirs.each do |project_dir|
85
+ next unless File.exist?(project_dir)
86
+
87
+ Dir.chdir(project_dir) do
88
+ hsh[project_dir] = branch_name(dir: project_dir)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # Returns the branch name associated with the given repository
95
+ # Defaults to the current directory
96
+ def branch_name(dir: default_project_dir)
97
+ return unless File.exist?(dir)
98
+
99
+ ::Git.open(dir).current_branch
100
+ end
101
+
102
+ # Returns true if the remote branch exists, false otherwise
103
+ def branch_exists?(project_dir, branch_name)
104
+ ::Git.ls_remote(project_dir)['remotes']["origin/#{branch_name}"]
105
+ end
106
+
107
+ # Prints the status of multiple repository directories and displays the results in a nice format
108
+ def status_all
109
+ @success = true
110
+ puts
111
+ puts 'Getting status in each repo'.light_yellow if project_dirs.length > 1
112
+ project_dirs.each do |project_dir|
113
+ next unless File.exist?(project_dir)
114
+
115
+ repo_basename = File.basename(File.realpath(project_dir))
116
+ header = " #{repo_basename} (#{original_branches[project_dir]}) "
117
+ puts center_pad(header).light_green
118
+ @success &= status(dir: project_dir)
119
+ puts center_pad.light_green
120
+ end
121
+ puts
122
+
123
+ raise 'Failed getting status on one or more repositories' unless @success
124
+ end
125
+
126
+ # Prints the results of the status command
127
+ # Currently running "git status" instead of using the library because it doesn't do well formatting the output
128
+ def status(dir: default_project_dir)
129
+ return unless File.exist?(dir)
130
+
131
+ # NOTE: git library doesn't have a good "status" analog. So just run the standard "git" one
132
+ # splitting and puts'ing to prefix each line with spaces...
133
+ Dir.chdir(dir) { indent `git status` }
134
+ end
135
+
136
+ # Returns the name of any repositories which have changes
137
+ def repos_with_changes
138
+ info.filter_map { |it| it.name unless changes(dir: it.path).empty? }
139
+ end
140
+
141
+ # Print the changes on the given repo
142
+ # Defaults to the current directory
143
+ def changes(dir: default_project_dir)
144
+ return unless File.exist?(dir)
145
+
146
+ Dir.chdir(dir) { `git status --porcelain | grep -v '^?'` }.split("\n").map(&:strip)
147
+ end
148
+
149
+ # Print the changes on the given repo using the ruby built-in method... which seems _REALLY_ slow compared to the porcelain version
150
+ # Defaults to the current directory
151
+ def changes_slow(dir: default_project_dir)
152
+ return unless File.exist?(dir)
153
+
154
+ s = ::Git.open(dir).status
155
+ s.added.keys.map { |it| " A #{it}" } +
156
+ s.changed.keys.map { |it| " M #{it}" } +
157
+ s.deleted.keys.map { |it| " D #{it}" }
158
+ end
159
+
160
+ # Runs a git reset on all given repositories with some additional formatting
161
+ def reset_all
162
+ puts
163
+ puts 'Resetting each repo'.light_yellow if project_dirs.length > 1
164
+ project_dirs.each do |project_dir|
165
+ next unless File.exist?(project_dir)
166
+
167
+ repo_basename = File.basename(File.realpath(project_dir))
168
+ header = " #{repo_basename} (#{original_branches[project_dir]}) "
169
+ puts center_pad(header).light_green
170
+ reset(dir: project_dir)
171
+ puts center_pad.light_green
172
+ end
173
+ puts
174
+ end
175
+
176
+ # Runs a git reset on the given repo
177
+ # Defaults to the current directory
178
+ def reset(dir: default_project_dir)
179
+ return unless File.exist?(dir)
180
+
181
+ g = ::Git.open(dir)
182
+ indent g.reset_hard
183
+ end
184
+
185
+ # Checks out the given branch in all repositories with some additional formatting
186
+ def checkout_all(branch)
187
+ @success = true
188
+ puts
189
+ puts "Checking out #{branch} in each repo".light_yellow if project_dirs.length > 1
190
+ project_dirs.each do |project_dir|
191
+ next unless File.exist?(project_dir)
192
+
193
+ repo_basename = File.basename(File.realpath(project_dir))
194
+ header = " #{repo_basename} "
195
+ puts center_pad(header).light_green
196
+ @success &= checkout(branch, dir: project_dir)
197
+ puts center_pad.light_green
198
+ end
199
+ puts
200
+
201
+ raise "Failed checking out branch #{branch} one or more repositories" unless @success
202
+ end
203
+
204
+ # Checks out the given branch in the given repo
205
+ # Defaults to the current directory
206
+ def checkout(branch, dir: default_project_dir)
207
+ raise 'branch is required' if branch.to_s.strip.empty?
208
+ return unless File.exist?(dir)
209
+
210
+ # Make sure the original branch hash has been created before we change anything
211
+ original_branches
212
+
213
+ g = ::Git.open(dir)
214
+ g.fetch('origin', prune: true)
215
+
216
+ # If the branch we are checking out doesn't exist, check out either the staging branch or the main branch
217
+ actual_branch = branch
218
+ unless branch_exists?(dir, branch)
219
+ actual_branch = [staging_branch, main_branch].uniq.find { |it| branch_exists?(dir, it) }
220
+ puts "Branch #{branch} not found, checking out #{actual_branch} instead".light_yellow
221
+ end
222
+
223
+ indent g.checkout(actual_branch)
224
+ indent g.pull('origin', actual_branch)
225
+ true
226
+ rescue ::Git::GitExecuteError => e
227
+ print_errors(e.message)
228
+ false
229
+ end
230
+
231
+ # Create the given branch in the given repo
232
+ def create_branch(branch, dir: default_project_dir)
233
+ raise 'branch is required' if branch.to_s.strip.empty?
234
+ raise "refusing to create protected branch '#{branch}'" if %w(master develop).any?(branch.to_s.strip)
235
+ return unless File.exist?(dir)
236
+
237
+ # Make sure the original branch hash has been created before we change anything
238
+ original_branches
239
+
240
+ g = ::Git.open(dir)
241
+ g.fetch('origin', prune: true)
242
+
243
+ puts "Fetching the latest changes for base branch #{staging_branch}"
244
+ g.checkout(staging_branch)
245
+ g.pull('origin', staging_branch)
246
+
247
+ puts "Creating branch #{branch}, pushing to origin, and updating remote tracking"
248
+ g.branch(branch).checkout
249
+ g.push('origin', branch)
250
+ g.config("branch.#{branch}.remote", 'origin')
251
+ g.config("branch.#{branch}.merge", "refs/heads/#{branch}")
252
+ puts
253
+ rescue ::Git::GitExecuteError => e
254
+ print_errors(e.message)
255
+ false
256
+ end
257
+
258
+ # Merge the branch into all repositories
259
+ def merge_all(branch)
260
+ @success = true
261
+ puts
262
+ puts "Merging #{branch} into each repo".light_yellow if project_dirs.length > 1
263
+ project_dirs.each do |project_dir|
264
+ next unless File.exist?(project_dir)
265
+
266
+ repo_basename = File.basename(File.realpath(project_dir))
267
+ header = " #{repo_basename} "
268
+ puts center_pad(header).light_green
269
+ @success &= merge(branch, dir: project_dir)
270
+ puts center_pad.light_green
271
+ end
272
+ puts
273
+
274
+ raise "Failed merging branch #{branch} in one or more repositories" unless @success
275
+
276
+ push_all
277
+ end
278
+
279
+ # Merge the given branch into the given repo
280
+ def merge(branch, dir: default_project_dir)
281
+ raise 'branch is required' if branch.to_s.strip.empty?
282
+ return unless File.exist?(dir)
283
+
284
+ # Make sure the original branch hash has been created before we change anything
285
+ original_branches
286
+
287
+ g = ::Git.open(dir)
288
+ g.fetch('origin', prune: true)
289
+ raise 'branch does not exist' unless branch_exists?(dir, branch)
290
+
291
+ # No need to merge into ourself
292
+ current_branch = branch_name(dir: dir)
293
+ return true if current_branch == branch
294
+
295
+ indent "Merging #{branch} into #{current_branch}"
296
+ indent g.merge(branch)
297
+ true
298
+ rescue ::Git::GitExecuteError => e
299
+ print_errors(e.message)
300
+ false
301
+ end
302
+
303
+ # Pull the latest in all repositories
304
+ def pull_all
305
+ @success = true
306
+ puts
307
+ puts 'Pulling current branch into each repo'.light_yellow if project_dirs.length > 1
308
+ project_dirs.each do |project_dir|
309
+ next unless File.exist?(project_dir)
310
+
311
+ repo_basename = File.basename(File.realpath(project_dir))
312
+ header = " #{repo_basename} "
313
+ puts center_pad(header).light_green
314
+ @success &= pull(dir: project_dir)
315
+ puts center_pad.light_green
316
+ end
317
+ puts
318
+
319
+ raise 'Failed pulling branch in one or more repositories' unless @success
320
+ end
321
+
322
+ # Pull the given repo
323
+ def pull(dir: default_project_dir)
324
+ return unless File.exist?(dir)
325
+
326
+ g = ::Git.open(dir)
327
+ g.fetch('origin', prune: true)
328
+
329
+ branch = branch_name(dir: dir)
330
+ indent "Pulling branch #{branch} from origin"
331
+ indent g.pull('origin', branch)
332
+ true
333
+ rescue ::Git::GitExecuteError => e
334
+ print_errors(e.message)
335
+ false
336
+ end
337
+
338
+ # Push to remote in all repositories
339
+ def push_all
340
+ @success = true
341
+ puts
342
+ puts 'Pushing current branch into each repo'.light_yellow if project_dirs.length > 1
343
+ project_dirs.each do |project_dir|
344
+ next unless File.exist?(project_dir)
345
+
346
+ repo_basename = File.basename(File.realpath(project_dir))
347
+ header = " #{repo_basename} "
348
+ puts center_pad(header).light_green
349
+ @success &= push(dir: project_dir)
350
+ puts center_pad.light_green
351
+ end
352
+ puts
353
+
354
+ raise 'Failed pushing branch in one or more repositories' unless @success
355
+ end
356
+
357
+ # Push the given repo
358
+ def push(dir: default_project_dir)
359
+ return unless File.exist?(dir)
360
+
361
+ g = ::Git.open(dir)
362
+ g.fetch('origin', prune: true)
363
+
364
+ branch = branch_name(dir: dir)
365
+ indent "Pushing branch #{branch} to origin"
366
+ indent g.push('origin', branch)
367
+ true
368
+ rescue ::Git::GitExecuteError => e
369
+ print_errors(e.message)
370
+ false
371
+ end
372
+
373
+ # Clones all repositories
374
+ def clone_repos
375
+ info.each { |it| clone_repo(dir: it.path, repo_name: it.name) }
376
+ end
377
+
378
+ # Clones the repo_name into the dir
379
+ # Optionally specify a repo_org
380
+ # Optionally specify a branch to check out (defaults to the repository default branch)
381
+ def clone_repo(dir:, repo_name:, repo_org: 'firespring', branch: nil)
382
+ if Dir.exist?("#{dir}/.git")
383
+ puts "#{dir} already cloned".light_green
384
+ return
385
+ end
386
+
387
+ FileUtils.mkdir_p(dir.to_s)
388
+
389
+ puts "Cloning #{dir} from #{ssh_repo_url(repo_name, repo_org)}".light_yellow
390
+
391
+ opts = {}
392
+ opts[:branch] = branch unless branch.to_s.strip.empty?
393
+ g = ::Git.clone(ssh_repo_url(repo_name, repo_org), dir, opts)
394
+ g.fetch('origin', prune: true)
395
+ end
396
+
397
+ # Builds an ssh repo URL using the org and repo name given
398
+ def ssh_repo_url(name, org)
399
+ "git@github.com:#{org}/#{name}.git"
400
+ end
401
+
402
+ # Split on newlines and add additional padding
403
+ def indent(string, padding: ' ')
404
+ string.to_s.split("\n").each { |line| puts "#{padding}#{line}" }
405
+ end
406
+
407
+ # Center the string and pad on either side with the given padding character
408
+ def center_pad(string = '', pad: '-', len: 80)
409
+ center_dash = len / 2
410
+ string = string.to_s
411
+ center_str = string.length / 2
412
+ string.rjust(center_dash + center_str - 1, pad).ljust(len - 1, pad)
413
+ end
414
+
415
+ # Exclude the command from the message and print all error lines
416
+ private def print_errors(message)
417
+ indent message.split('error:')[1..].join
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,33 @@
1
+ module Dev
2
+ class Jira
3
+ # Contains information and methods representing a Jira issue
4
+ class Issue
5
+ # Issue subtypes which do not map to a story type
6
+ NON_STORY_TYPES = ['review', 'sub-task', 'code review sub-task', 'pre-deploy sub-task', 'deploy sub-task', 'devops sub-task'].freeze
7
+
8
+ attr_accessor :data, :project, :id, :title, :points, :assignee, :resolved_date
9
+
10
+ def initialize(data)
11
+ @data = data
12
+ @project = Jira::Project.new(data)
13
+ @id = data.key
14
+ @title = data.summary
15
+ @points = calculate_points(data)
16
+ @assignee = Jira::User.lookup(data.assignee&.accountId)
17
+ @resolved_date = data.resolutiondate
18
+ end
19
+
20
+ # Returns the value of the jira points field or 0 if the field is not found
21
+ def calculate_points(data)
22
+ return data.send(Dev::Jira.config.points_field_name).to_i if Dev::Jira.config.points_field_name && data.respond_to?(Dev::Jira.config.points_field_name)
23
+
24
+ 0
25
+ end
26
+
27
+ # Converts the jira issue object to a string representation
28
+ def to_s
29
+ "[#{id}] #{title} (#{points} pts) (resolved #{resolved_date}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ module Dev
2
+ class Jira
3
+ # Contains information on the Jira project
4
+ class Project
5
+ attr_accessor :name
6
+
7
+ def initialize(data)
8
+ @name = data.project.name
9
+ @name = @name << ' DevOps' if /devops/i.match?(data.issuetype.name)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Dev
2
+ class Jira
3
+ class User
4
+ # Contains constants representing valid Jira user types
5
+ class Type
6
+ # Bot type
7
+ BOT = :bot
8
+
9
+ # Developer type
10
+ DEVELOPER = :developer
11
+
12
+ # "Other" type
13
+ OTHER = :other
14
+
15
+ # Project manager type
16
+ PM = :pm
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ module Dev
2
+ class Jira
3
+ # Contains Jira user information
4
+ # Jira does not make user information available through their normal api (they have an admin api that you can use)
5
+ # Therefore, we've provided a "lookup" method which attempts to find the jira user id in a hash of user information
6
+ # that you can configure
7
+ class User
8
+ attr_accessor :name, :email, :id, :type
9
+
10
+ def initialize(name:, email:, id:, type: Type::OTHER)
11
+ @name = name
12
+ @email = email
13
+ @id = id
14
+ @type = type
15
+ end
16
+
17
+ # Returns true if the Jira user is categorized as a developer
18
+ def developer?
19
+ type == Type::DEVELOPER
20
+ end
21
+
22
+ # Returns the Jira user object which maps to the give user id
23
+ # If none is found, it returns a Jira user object with only the id set
24
+ def self.lookup(id)
25
+ user = Dev::Jira.config.user_lookup_list&.find { |it| it.id == id }
26
+ user ||= new(name: '', email: '', id: id)
27
+ user
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,78 @@
1
+ require 'jira-ruby'
2
+ require 'base64'
3
+
4
+ module Dev
5
+ # Class which contains methods to conenct to jira and query issues
6
+ class Jira
7
+ # Config object for setting top level jira config options
8
+ # "points_field_name" is the field holding the value for points on a story. If this is not present, all points will default to 0
9
+ # "user_lookup_list" should be an array of Jira::User objects representing the usernames, ids, etc for all jira users
10
+ # This is a bit clumsy but currently the jira api only returns the user id with issues
11
+ # and there is no way to query this information from Jira directly.
12
+ Config = Struct.new(:username, :token, :url, :points_field_name, :user_lookup_list, :read_timeout, :http_debug) do
13
+ def initialize
14
+ self.username = nil
15
+ self.token = nil
16
+ self.url = nil
17
+ self.points_field_name = nil
18
+ self.user_lookup_list = []
19
+ self.read_timeout = 120
20
+ self.http_debug = false
21
+ end
22
+ end
23
+
24
+ class << self
25
+ # Instantiates a new top level config object if one hasn't already been created
26
+ # Yields that config object to any given block
27
+ # Returns the resulting config object
28
+ def config
29
+ @config ||= Config.new
30
+ yield(@config) if block_given?
31
+ @config
32
+ end
33
+
34
+ # Alias the config method to configure for a slightly clearer access syntax
35
+ alias_method :configure, :config
36
+ end
37
+
38
+ attr_accessor :username, :token, :url, :auth, :client
39
+
40
+ # Initialize a new jira client using the given inputs
41
+ def initialize(username: self.class.config.username, token: self.class.config.token, url: self.class.config.url)
42
+ @username = username
43
+ @token = token
44
+ @url = url
45
+ @auth = Base64.strict_encode64("#{@username}:#{@token}")
46
+
47
+ options = {
48
+ auth_type: :basic,
49
+ site: @url,
50
+ default_headers: {Authorization: "Basic #{@auth}"},
51
+ context_path: '',
52
+ read_timeout: self.class.config.read_timeout,
53
+ use_ssl: true,
54
+ ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
55
+ http_debug: self.class.config.http_debug
56
+ }
57
+
58
+ @client = JIRA::Client.new(options)
59
+ end
60
+
61
+ # Query jira using the given jql and yield each matching result
62
+ def issues(jql, &block)
63
+ start_at = 0
64
+ max_results = 100
65
+
66
+ # Query Jira and yield all issues it returns
67
+ issues = @client.Issue.jql(jql, start_at: start_at, max_results: max_results)
68
+ issues.map { |data| Issue.new(data) }.each(&block)
69
+
70
+ # If we returned the max_results then there may be more - add the max results to where we start at and query again
71
+ while issues.length >= max_results
72
+ start_at += max_results
73
+ issues = @client.Issue.jql(jql, start_at: start_at, max_results: max_results)
74
+ issues.map { |data| Issue.new(data) }.each(&block)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,8 @@
1
+ require 'logger'
2
+
3
+ # Only define LOG if it doesn't already exist
4
+ unless defined? LOG
5
+ LOG = Logger.new($stdout)
6
+ LOG.formatter = proc { |_, _, _, msg| "#{msg}\n" }
7
+ LOG.level = Logger::DEBUG
8
+ end
@@ -0,0 +1,39 @@
1
+ module Dev
2
+ class Node
3
+ # Class which contains commands and customizations for node security audit reports
4
+ class Audit
5
+ attr_accessor :data
6
+
7
+ def initialize(data)
8
+ @data = JSON.parse(Dev::Common.new.strip_non_json(data))
9
+ end
10
+
11
+ # Convert the node audit data to the standardized audit report object
12
+ def to_report
13
+ ids = Set.new
14
+
15
+ Dev::Audit::Report.new(
16
+ data['vulnerabilities'].map do |_, vulnerability|
17
+ # If the via ia a hash and the id is not already recorded, add the item to our report
18
+ vulnerability['via'].map do |it|
19
+ next unless it.is_a?(Hash)
20
+
21
+ id = it['url']&.split('/')&.last
22
+ next if ids.include?(id)
23
+
24
+ ids << id
25
+ Dev::Audit::Report::Item.new(
26
+ id: id,
27
+ name: vulnerability['name'],
28
+ title: it['title'],
29
+ url: it['url'],
30
+ severity: vulnerability['severity'],
31
+ version: it['range']
32
+ )
33
+ end
34
+ end.flatten.compact
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end