spektr 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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +32 -0
  3. data/.gitignore +9 -0
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +3 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +134 -0
  9. data/Guardfile +45 -0
  10. data/LICENSE.txt +27 -0
  11. data/README.md +70 -0
  12. data/Rakefile +10 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/bin/spektr +7 -0
  16. data/lib/spektr/app.rb +209 -0
  17. data/lib/spektr/checks/base.rb +151 -0
  18. data/lib/spektr/checks/basic_auth.rb +27 -0
  19. data/lib/spektr/checks/basic_auth_timing.rb +24 -0
  20. data/lib/spektr/checks/command_injection.rb +48 -0
  21. data/lib/spektr/checks/content_tag_xss.rb +54 -0
  22. data/lib/spektr/checks/cookie_serialization.rb +21 -0
  23. data/lib/spektr/checks/create_with.rb +27 -0
  24. data/lib/spektr/checks/csrf.rb +25 -0
  25. data/lib/spektr/checks/csrf_setting.rb +39 -0
  26. data/lib/spektr/checks/default_routes.rb +43 -0
  27. data/lib/spektr/checks/deserialize.rb +62 -0
  28. data/lib/spektr/checks/detailed_exceptions.rb +29 -0
  29. data/lib/spektr/checks/digest_dos.rb +28 -0
  30. data/lib/spektr/checks/dynamic_finders.rb +26 -0
  31. data/lib/spektr/checks/evaluation.rb +25 -0
  32. data/lib/spektr/checks/file_access.rb +38 -0
  33. data/lib/spektr/checks/file_disclosure.rb +25 -0
  34. data/lib/spektr/checks/filter_skipping.rb +29 -0
  35. data/lib/spektr/checks/header_dos.rb +20 -0
  36. data/lib/spektr/checks/i18n_xss.rb +20 -0
  37. data/lib/spektr/checks/json_encoding.rb +23 -0
  38. data/lib/spektr/checks/json_entity_escape.rb +30 -0
  39. data/lib/spektr/checks/json_parsing.rb +47 -0
  40. data/lib/spektr/checks/link_to_href.rb +35 -0
  41. data/lib/spektr/checks/mass_assignment.rb +42 -0
  42. data/lib/spektr/checks/send.rb +24 -0
  43. data/lib/spektr/checks/sqli.rb +52 -0
  44. data/lib/spektr/checks/xss.rb +49 -0
  45. data/lib/spektr/checks.rb +9 -0
  46. data/lib/spektr/cli.rb +53 -0
  47. data/lib/spektr/erubi.rb +78 -0
  48. data/lib/spektr/exp/assignment.rb +20 -0
  49. data/lib/spektr/exp/base.rb +32 -0
  50. data/lib/spektr/exp/const.rb +7 -0
  51. data/lib/spektr/exp/definition.rb +32 -0
  52. data/lib/spektr/exp/ivasign.rb +7 -0
  53. data/lib/spektr/exp/lvasign.rb +7 -0
  54. data/lib/spektr/exp/send.rb +135 -0
  55. data/lib/spektr/exp/xstr.rb +12 -0
  56. data/lib/spektr/processors/base.rb +80 -0
  57. data/lib/spektr/processors/class_processor.rb +25 -0
  58. data/lib/spektr/targets/base.rb +119 -0
  59. data/lib/spektr/targets/config.rb +6 -0
  60. data/lib/spektr/targets/controller.rb +74 -0
  61. data/lib/spektr/targets/model.rb +6 -0
  62. data/lib/spektr/targets/routes.rb +38 -0
  63. data/lib/spektr/targets/view.rb +34 -0
  64. data/lib/spektr/version.rb +3 -0
  65. data/lib/spektr/warning.rb +23 -0
  66. data/lib/spektr.rb +120 -0
  67. data/spektr.gemspec +49 -0
  68. metadata +362 -0
@@ -0,0 +1,135 @@
1
+ module Spektr
2
+ module Exp
3
+ class Send < Base
4
+ attr_accessor :receiver
5
+
6
+ def initialize(ast)
7
+ super
8
+ @receiver = Receiver.new(ast.children[0]) if ast.children[0]
9
+ @name = if ast.children.first.is_a?(Parser::AST::Node)
10
+ ast.children.first.children.last
11
+ else
12
+ ast.children[1]
13
+ end
14
+ children = ast.children[2..]
15
+ children.each do |child|
16
+ next unless child.is_a?(Parser::AST::Node)
17
+
18
+ case child.type
19
+ when :hash
20
+ if children.size == 1 || children.last == child
21
+ child.children.each do |pair|
22
+ @options[pair.children[0].children[0]] = Option.new(pair)
23
+ end
24
+ else
25
+ @arguments << Argument.new(child)
26
+ end
27
+ else
28
+ @arguments << Argument.new(child)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class Argument < Base
35
+ attr_accessor :name, :type, :ast, :children
36
+
37
+ def initialize(ast)
38
+ @name = nil
39
+ process_ast(ast)
40
+ ast = ast.children.first if ast.type == :begin
41
+ @ast = ast
42
+ argument = if %i[xstr hash].include? ast.type
43
+ ast
44
+ elsif ast.type != :dstr && ast.children.first.is_a?(Parser::AST::Node) && ast.children.first.children.first.is_a?(Parser::AST::Node)
45
+ ast.children.first.children.first
46
+ elsif ast.type != :dstr && ast.children.first.is_a?(Parser::AST::Node)
47
+ ast.children.first
48
+ else
49
+ ast
50
+ end
51
+ @type = argument.type
52
+ @children = argument.children
53
+ end
54
+
55
+ def process_ast(ast)
56
+ process(ast)
57
+ end
58
+
59
+ def on_begin(node)
60
+ process_all(node)
61
+ end
62
+
63
+ def on_send(node)
64
+ if node.children.first.nil?
65
+ @name ||= node.children[1]
66
+ elsif node.is_a?(Parser::AST::Node)
67
+ process_all(node)
68
+ end
69
+ end
70
+
71
+ def on_const(node)
72
+ @name ||= node.children[1] if node.children.first.nil?
73
+ end
74
+
75
+ def on_str(node)
76
+ @name ||= node.children.first
77
+ end
78
+
79
+ alias on_sym on_str
80
+ alias on_ivar on_str
81
+ end
82
+
83
+ class Option
84
+ attr_accessor :name, :key, :value, :type, :value_name, :value_type
85
+
86
+ def initialize(ast)
87
+ @key = ast.children.first
88
+ @name = ast.children.first.children.last
89
+ @type = ast.type
90
+
91
+ @value = ast.children.last
92
+ @value_name = ast.children.last.children.last
93
+ @value_type = ast.children.last.type
94
+ end
95
+ end
96
+
97
+ class Receiver
98
+ attr_accessor :name, :type, :ast, :expanded, :children
99
+
100
+ def initialize(ast)
101
+ @children = []
102
+ set_attributes(ast)
103
+ end
104
+
105
+ def set_attributes(ast)
106
+ if [:begin].include?(ast.type) && ast.children[0].is_a?(Parser::AST::Node)
107
+ return set_attributes(ast.children[0])
108
+ end
109
+
110
+ @ast = ast
111
+ if ast.type == :dstr
112
+ @type = :dstr
113
+ @name = ast.children[0].children.first
114
+ ast.children[1..].each do |ch|
115
+ @children << Receiver.new(ch)
116
+ end
117
+ else
118
+ @expanded = expand!(ast)
119
+ @ast = ast.type == :send && ast.children[0].is_a?(Parser::AST::Node) ? ast.children[0] : ast
120
+ @type = @ast.type
121
+ @name = @ast.children.last
122
+ end
123
+ end
124
+
125
+ def expand!(ast, tree = [])
126
+ if ast.is_a?(Parser::AST::Node) && ast.children.any?
127
+ tree << ast.children.last
128
+ expand!(ast.children.first, tree)
129
+ else
130
+ tree.reverse.join('.')
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,12 @@
1
+ module Spektr
2
+ module Exp
3
+ class Xstr < Base
4
+ def initialize(ast)
5
+ super
6
+ ast.children[1..].each do |child|
7
+ @arguments << Argument.new(child)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,80 @@
1
+ require 'ast'
2
+ module Spektr::Processors
3
+ class Base
4
+ include AST::Processor::Mixin
5
+ attr_accessor :name, :name_parts, :parent_parts, :parent_modules, :parent_name, :parent_name_with_modules
6
+
7
+ def initialize
8
+ @modules = []
9
+ @name_parts = []
10
+ @parent_parts = []
11
+ @parent_modules = []
12
+ end
13
+
14
+ def name
15
+ @name_parts.join('::')
16
+ end
17
+
18
+ def parent_name
19
+ @parent_parts.shift if @parent_parts.first.to_s == name
20
+ @parent_parts.join('::')
21
+ end
22
+
23
+ def parent_name_with_modules
24
+ parts = @parent_modules | @parent_parts
25
+ parts.shift if parts.first.to_s == name
26
+ parts.join('::')
27
+ end
28
+
29
+ def on_begin(node)
30
+ process_all(node)
31
+ end
32
+
33
+ def on_module(node)
34
+ parts = extract_name_part(node)
35
+ @modules.concat(parts)
36
+ @name_parts.concat(parts)
37
+ @parent_modules << node.children.first.children.last
38
+ process_all(node)
39
+ end
40
+
41
+ def extract_parent_parts(node)
42
+ if node.children[1] && node.children[1].is_a?(Parser::AST::Node)
43
+ node.children[1].children.each do |child|
44
+ if child.is_a?(Parser::AST::Node)
45
+ extract_parent_parts(child)
46
+ @parent_parts << child.children.last
47
+ elsif child.is_a? Symbol
48
+ @parent_parts << child.to_s
49
+ end
50
+ end
51
+ elsif node&.children&.first&.children&.last
52
+ @parent_parts << node.children.first.children.last
53
+ end
54
+ end
55
+
56
+ def on_class(node)
57
+ extract_parent_parts(node)
58
+ @name_parts.concat(extract_name_part(node))
59
+ process_all(node)
60
+ end
61
+
62
+ def extract_name_part(node)
63
+ parts = []
64
+ node.children.first.children.each do |child|
65
+ if child.is_a?(Parser::AST::Node)
66
+ parts << child.children.last
67
+ elsif child.is_a? Symbol
68
+ parts << child.to_s
69
+ end
70
+ end
71
+ parts
72
+ end
73
+
74
+ def on_const(node); end
75
+
76
+ def handler_missing(node)
77
+ # puts "handler missing for #{node.type}"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,25 @@
1
+ module Spektr
2
+ module Processors
3
+ class ClassProcessor < Base
4
+ include AST::Processor::Mixin
5
+
6
+ attr_accessor :data
7
+
8
+ def on_begin(node)
9
+ end
10
+
11
+ def on_def(node)
12
+ puts "on def: #{node.inspect}"
13
+ end
14
+
15
+ def on_require(node)
16
+ puts "on require: #{node.inspect}"
17
+ end
18
+
19
+ def on_class(node)
20
+ debugger
21
+ puts "on class2: #{node.inspect}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,119 @@
1
+ module Spektr
2
+ module Targets
3
+ class Base
4
+ attr_accessor :path, :name, :options, :ast, :parent, :processor
5
+
6
+ def initialize(path, content)
7
+ Spektr.logger.debug "loading #{path}"
8
+ @ast = Spektr::App.parser.parse(content)
9
+ @path = path
10
+ return unless @ast
11
+
12
+ @processor = Spektr::Processors::Base.new
13
+ @processor.process(@ast)
14
+ @name = @processor.name
15
+ @name = @path.split('/').last if @name.blank?
16
+
17
+ @current_method_type = :public
18
+ @parent = @processor.parent_name
19
+ end
20
+
21
+ def find_calls(name, receiver = nil)
22
+ calls = find(:send, name, @ast).map { |ast| Exp::Send.new(ast) }
23
+ if receiver
24
+ calls.select! { |call| call.receiver&.expanded == receiver }
25
+ elsif receiver == false
26
+ calls.select! { |call| call.receiver.nil? }
27
+ end
28
+ calls
29
+ end
30
+
31
+ def find_calls_with_block(name, _receiver = nil)
32
+ blocks = find(:block, nil, @ast)
33
+ blocks.each_with_object([]) do |block, memo|
34
+ if block.children.first.children[1] == name
35
+ result = find(:send, name, block).map { |ast| Exp::Send.new(ast) }
36
+ memo << result.first
37
+ end
38
+ end
39
+ end
40
+
41
+ def find_method(name)
42
+ find(:def, name, @ast).last
43
+ end
44
+
45
+ def find_xstr
46
+ find(:xstr, nil, @ast).map { |ast| Exp::Xstr.new(ast) }
47
+ end
48
+
49
+ def find(type, name, ast, result = [])
50
+ return result unless ast.is_a? Parser::AST::Node
51
+
52
+ name_index = case type
53
+ when :def
54
+ 0
55
+ else
56
+ 1
57
+ end
58
+ if node_matches?(ast.type, ast.children[name_index], type, name)
59
+ result << ast
60
+ elsif ast.children.any?
61
+ ast.children.each do |child|
62
+ result = find(type, name, child, result)
63
+ end
64
+ end
65
+ result
66
+ end
67
+
68
+ def node_matches?(node_type, node_name, type, name)
69
+ if node_type == type
70
+ if name.is_a? Regexp
71
+ return node_name =~ name
72
+ elsif name.nil?
73
+ return true
74
+ else
75
+ return node_name == name
76
+ end
77
+ end
78
+ false
79
+ end
80
+
81
+ def find_methods(ast:, result: [], type: :all)
82
+ return result unless ast.is_a?(Parser::AST::Node)
83
+
84
+ if ast.type == :send && %i[private public protected].include?(ast.children.last)
85
+ @current_method_type = ast.children.last
86
+ end
87
+ if ast.type == :def && [:all, @current_method_type].include?(type)
88
+ result << ast
89
+ elsif ast.children.any?
90
+ ast.children.map do |child|
91
+ result = find_methods(ast: child, result: result, type: type)
92
+ end
93
+ end
94
+ result
95
+ end
96
+
97
+ def ast_to_exp(ast)
98
+ case ast.type
99
+ when :send
100
+ Exp::Send.new(ast)
101
+ when :def
102
+ Exp::Definition.new(ast)
103
+ when :ivasgn
104
+ Exp::Ivasgin.new(ast)
105
+ when :lvasign
106
+ Exp::Lvasign.new(ast)
107
+ when :const
108
+ Exp::Const.new(ast)
109
+ when :xstr
110
+ Exp::Xstr.new(ast)
111
+ when :sym, :int, :str
112
+ Exp::Base.new(ast)
113
+ else
114
+ raise "Unknown type #{ast.type} #{ast.inspect}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,6 @@
1
+ module Spektr
2
+ module Targets
3
+ class Config < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,74 @@
1
+ module Spektr
2
+ module Targets
3
+ class Controller < Base
4
+ attr_accessor :actions
5
+
6
+ def initialize(path, content)
7
+ super
8
+ find_actions
9
+ end
10
+
11
+ def concern?
12
+ !name.match('Controller')
13
+ end
14
+
15
+ def find_actions
16
+ @actions = find_methods(ast: @ast, type: :public).map do |ast|
17
+ Action.new(ast, self)
18
+ end
19
+ end
20
+
21
+ def find_parent(controllers)
22
+ parent_name = @processor.parent_name
23
+ result = find_in_set(parent_name, controllers)
24
+ result ||= find_in_set(processor.parent_name_with_modules, controllers)
25
+ return nil if result&.name == name
26
+
27
+ result
28
+ end
29
+
30
+ def find_in_set(name, set)
31
+ while true
32
+ result = set.find { |c| c.name == name }
33
+ break if result
34
+
35
+ split = name.split('::')
36
+ split.shift
37
+ name = split.join('::')
38
+ break if name.blank?
39
+ end
40
+ result
41
+ end
42
+
43
+ class Action < Spektr::Exp::Definition
44
+ attr_accessor :controller, :template
45
+
46
+ def initialize(ast, controller)
47
+ super(ast)
48
+ @template = nil
49
+ split = []
50
+ if controller.parent
51
+ split = controller.parent.split('::').map { |e| e.delete_suffix('Controller') }.map(&:downcase)
52
+ if split.size > 1
53
+ split.pop
54
+ @template = "#{split.join('/')}/#{@template}"
55
+ end
56
+ end
57
+ split = split.concat(controller.name.delete_suffix('Controller').split('::').map(&:downcase)).uniq
58
+ split.delete('application')
59
+ @template = File.join(*split, name.to_s)
60
+ @body.each do |exp|
61
+ if exp.send? && (exp.name == :render && exp.arguments.any?)
62
+ if exp.arguments.first.type == :sym
63
+ @template = File.join(controller.name.delete_suffix('Controller').underscore,
64
+ exp.arguments.first.name.to_s)
65
+ elsif exp.arguments.first.type == :str
66
+ @template = exp.arguments.first.name
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,6 @@
1
+ module Spektr
2
+ module Targets
3
+ class Model < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,38 @@
1
+ module Spektr
2
+ module Targets
3
+ class Routes < Base
4
+ attr_accessor :routes
5
+
6
+ def initialize(path, content)
7
+ super
8
+ end
9
+
10
+ def find_actions
11
+ @actions = find_methods(ast: @ast, type: :public ).map do |ast|
12
+ Action.new(ast, self)
13
+ end
14
+ end
15
+
16
+ class Action < Spektr::Exp::Definition
17
+ attr_accessor :controller, :template
18
+ def initialize(ast, controller)
19
+ super(ast)
20
+ @template = File.join(controller.name.delete_suffix("Controller").underscore, name.to_s)
21
+ @body.each do |exp|
22
+ if exp.send?
23
+ if exp.name == :render
24
+ if exp.arguments.first.type == :sym
25
+ @template = File.join(controller.name.delete_suffix("Controller").underscore, exp.arguments.first.name.to_s)
26
+ elsif
27
+ if exp.arguments.first.type == :str
28
+ @template = exp.arguments.first.name
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ module Spektr
2
+ module Targets
3
+ class View < Base
4
+ TEMPLATE_EXTENSIONS = /.*\.(erb|rhtml|haml)$/
5
+ attr_accessor :view_path
6
+
7
+ def initialize(path, content)
8
+ Spektr.logger.debug "loading #{path}"
9
+ @view_path = nil
10
+ @path = path
11
+ if match_data = path.match(%r{views/(.+?)\.})
12
+ @view_path = match_data[1]
13
+ end
14
+ begin
15
+ @ast = Spektr::App.parser.parse(source(content))
16
+ rescue Parser::SyntaxError => e
17
+ @ast = Spektr::App.parser.parse('')
18
+ ::Spektr.logger.error "Parser::SyntaxError when parsing #{@view_path}: #{e.message}"
19
+ end
20
+ @name = @view_path # @ast.children.first.children.last.to_s
21
+ end
22
+
23
+ def source(content)
24
+ type = @path.match(TEMPLATE_EXTENSIONS)[1].to_sym
25
+ case type
26
+ when :erb, :rhtml
27
+ Erubi.new(content, trim_mode: '-').src
28
+ when :haml
29
+ Haml::Engine.new(content).precompiled
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Spektr
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ module Spektr
2
+ class Warning
3
+ attr_accessor :path, :full_path, :check, :location, :message, :confidence, :line
4
+ def initialize(path, full_path, check, location, message, confidence = :high)
5
+ @path = path
6
+ @check = check
7
+ @location = location
8
+ @message = message
9
+ @confidence = confidence
10
+ if full_path && @location && File.exist?(full_path)
11
+ @line = IO.readlines(full_path)[@location.line - 1].strip
12
+ end
13
+ end
14
+
15
+ def full_message
16
+ if @location
17
+ "#{message} at line #{@location.line} of #{@path}"
18
+ else
19
+ "#{message}"
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/spektr.rb ADDED
@@ -0,0 +1,120 @@
1
+ require 'spektr/version'
2
+ require 'bundler'
3
+ require 'parser'
4
+ require 'parser/current'
5
+ require 'unparser'
6
+ require 'erb'
7
+ require 'haml'
8
+ require 'active_support/core_ext/string/inflections'
9
+ require 'logger'
10
+ require 'tty/spinner'
11
+ require 'tty/table'
12
+
13
+ require 'zeitwerk'
14
+ loader = Zeitwerk::Loader.for_gem
15
+ loader.collapse("#{__dir__}/processors")
16
+ loader.setup # ready!
17
+ loader.eager_load
18
+
19
+ module Spektr
20
+ class Error < StandardError; end
21
+
22
+ def self.run(root = nil, output_format = 'terminal', debug = false, checks = nil)
23
+ pastel = Pastel.new
24
+ @output_format = output_format
25
+ start_spinner('Initializing')
26
+ @log_level = debug ? Logger::DEBUG : Logger::WARN
27
+ checks = Checks.load(checks)
28
+ root = './' if root.nil?
29
+ @app = App.new(checks: checks, root: root)
30
+ stop_spinner
31
+ puts "\n"
32
+ puts pastel.bold('Checks:')
33
+ puts "\n"
34
+ puts checks.collect(&:name).join(', ')
35
+ # table = TTY::Table.new([['Checks', checks.collect(&:name).join(', ')]])
36
+ # puts table.render(:basic)
37
+ puts "\n"
38
+
39
+ start_spinner('Loading files')
40
+ @app.load
41
+ stop_spinner
42
+ puts "\n"
43
+ table = TTY::Table.new([
44
+ ['Rails version', @app.rails_version],
45
+ ['Initializers', @app.initializers.size],
46
+ ['Controllers', @app.controllers.size],
47
+ ['Models', @app.models.size],
48
+ ['Views', @app.views.size],
49
+ ['Routes', @app.routes.size],
50
+ ['Lib files', @app.lib_files.size]
51
+ ])
52
+ puts table.render(:basic)
53
+ puts "\n"
54
+ start_spinner('Scanning files')
55
+ @app.scan!
56
+ stop_spinner
57
+ puts "\n"
58
+ json = @app.report
59
+ case output_format
60
+ when 'json'
61
+ json
62
+ when 'terminal'
63
+ puts pastel.bold("Advisories\n")
64
+
65
+ json[:advisories].each do |advisory|
66
+ puts "#{pastel.green('Name:')} #{advisory[:name]}\n"
67
+ puts "#{pastel.green('Check:')} #{advisory[:check]}\n"
68
+ puts "#{pastel.green('Description:')} #{advisory[:description]}\n"
69
+ puts "#{pastel.green('Path:')} #{advisory[:path]}\n"
70
+ puts "#{pastel.green('Location:')} #{advisory[:location]}\n"
71
+ puts "#{pastel.green('Code:')} #{advisory[:line]}\n"
72
+ puts "\n"
73
+ puts "\n"
74
+ end
75
+
76
+ puts pastel.bold("Summary\n")
77
+ summary = []
78
+ json[:advisories].group_by { |a| a[:name] }.each do |n, i|
79
+ summary << [pastel.green(n), i.size]
80
+ end
81
+
82
+ table = TTY::Table.new(summary, padding: [2, 2, 2, 2])
83
+ puts table.render(:basic)
84
+ puts "\n\n"
85
+ exit 1 if json[:advisories].any?
86
+ else
87
+ puts 'Unknown format'
88
+ end
89
+ end
90
+
91
+ def self.terminal?
92
+ @output_format == 'terminal'
93
+ end
94
+
95
+ def self.start_spinner(label)
96
+ return unless terminal?
97
+
98
+ @spinner = TTY::Spinner.new("[:spinner] #{label}", format: :classic)
99
+ @spinner.auto_spin
100
+ end
101
+
102
+ def self.stop_spinner
103
+ return unless terminal?
104
+
105
+ @spinner&.stop('Done!')
106
+ end
107
+
108
+ def self.swap_spinner(label)
109
+ stop_spinner
110
+ start_spinner(label)
111
+ end
112
+
113
+ def self.logger
114
+ @logger ||= begin
115
+ logger = Logger.new(STDOUT)
116
+ logger.level = @log_level || Logger::WARN
117
+ logger
118
+ end
119
+ end
120
+ end