spektr 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/.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
|