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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +32 -0
- data/.gitignore +9 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +134 -0
- data/Guardfile +45 -0
- data/LICENSE.txt +27 -0
- data/README.md +70 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/spektr +7 -0
- data/lib/spektr/app.rb +209 -0
- data/lib/spektr/checks/base.rb +151 -0
- data/lib/spektr/checks/basic_auth.rb +27 -0
- data/lib/spektr/checks/basic_auth_timing.rb +24 -0
- data/lib/spektr/checks/command_injection.rb +48 -0
- data/lib/spektr/checks/content_tag_xss.rb +54 -0
- data/lib/spektr/checks/cookie_serialization.rb +21 -0
- data/lib/spektr/checks/create_with.rb +27 -0
- data/lib/spektr/checks/csrf.rb +25 -0
- data/lib/spektr/checks/csrf_setting.rb +39 -0
- data/lib/spektr/checks/default_routes.rb +43 -0
- data/lib/spektr/checks/deserialize.rb +62 -0
- data/lib/spektr/checks/detailed_exceptions.rb +29 -0
- data/lib/spektr/checks/digest_dos.rb +28 -0
- data/lib/spektr/checks/dynamic_finders.rb +26 -0
- data/lib/spektr/checks/evaluation.rb +25 -0
- data/lib/spektr/checks/file_access.rb +38 -0
- data/lib/spektr/checks/file_disclosure.rb +25 -0
- data/lib/spektr/checks/filter_skipping.rb +29 -0
- data/lib/spektr/checks/header_dos.rb +20 -0
- data/lib/spektr/checks/i18n_xss.rb +20 -0
- data/lib/spektr/checks/json_encoding.rb +23 -0
- data/lib/spektr/checks/json_entity_escape.rb +30 -0
- data/lib/spektr/checks/json_parsing.rb +47 -0
- data/lib/spektr/checks/link_to_href.rb +35 -0
- data/lib/spektr/checks/mass_assignment.rb +42 -0
- data/lib/spektr/checks/send.rb +24 -0
- data/lib/spektr/checks/sqli.rb +52 -0
- data/lib/spektr/checks/xss.rb +49 -0
- data/lib/spektr/checks.rb +9 -0
- data/lib/spektr/cli.rb +53 -0
- data/lib/spektr/erubi.rb +78 -0
- data/lib/spektr/exp/assignment.rb +20 -0
- data/lib/spektr/exp/base.rb +32 -0
- data/lib/spektr/exp/const.rb +7 -0
- data/lib/spektr/exp/definition.rb +32 -0
- data/lib/spektr/exp/ivasign.rb +7 -0
- data/lib/spektr/exp/lvasign.rb +7 -0
- data/lib/spektr/exp/send.rb +135 -0
- data/lib/spektr/exp/xstr.rb +12 -0
- data/lib/spektr/processors/base.rb +80 -0
- data/lib/spektr/processors/class_processor.rb +25 -0
- data/lib/spektr/targets/base.rb +119 -0
- data/lib/spektr/targets/config.rb +6 -0
- data/lib/spektr/targets/controller.rb +74 -0
- data/lib/spektr/targets/model.rb +6 -0
- data/lib/spektr/targets/routes.rb +38 -0
- data/lib/spektr/targets/view.rb +34 -0
- data/lib/spektr/version.rb +3 -0
- data/lib/spektr/warning.rb +23 -0
- data/lib/spektr.rb +120 -0
- data/spektr.gemspec +49 -0
- 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,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,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,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,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
|