lint_fu 0.5.0 → 0.5.3
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.
- data/README.rdoc +4 -2
- data/bin/lint_fu +2 -86
- data/lib/lint_fu/blessing.rb +40 -0
- data/lib/lint_fu/checker.rb +49 -3
- data/lib/lint_fu/cli/command.rb +48 -0
- data/lib/lint_fu/cli/prune.rb +78 -0
- data/lib/lint_fu/cli/scan.rb +78 -0
- data/lib/lint_fu/cli.rb +52 -0
- data/lib/lint_fu/eidos.rb +35 -0
- data/lib/lint_fu/{model_element_builder.rb → eidos_builder.rb} +8 -3
- data/lib/lint_fu/eidos_container.rb +25 -0
- data/lib/lint_fu/file_range.rb +43 -0
- data/lib/lint_fu/mixins/sexp_instance_methods.rb +57 -9
- data/lib/lint_fu/parser.rb +1 -2
- data/lib/lint_fu/plugins/action_pack/controller_eidos.rb +7 -0
- data/lib/lint_fu/{action_pack/model_controller_builder.rb → plugins/action_pack/controller_eidos_builder.rb} +6 -6
- data/lib/lint_fu/plugins/action_pack.rb +2 -0
- data/lib/lint_fu/{active_record/model_model.rb → plugins/active_record/model_eidos.rb} +3 -3
- data/lib/lint_fu/{active_record/model_model_builder.rb → plugins/active_record/model_eidos_builder.rb} +17 -11
- data/lib/lint_fu/plugins/active_record.rb +2 -0
- data/lib/lint_fu/{rails → plugins/rails}/buggy_eager_load_checker.rb +6 -5
- data/lib/lint_fu/{rails/scan_builder.rb → plugins/rails/issue_builder.rb} +9 -16
- data/lib/lint_fu/plugins/rails/model_application.rb +21 -0
- data/lib/lint_fu/plugins/rails/model_application_factory.rb +31 -0
- data/lib/lint_fu/{rails → plugins/rails}/sql_injection_checker.rb +9 -5
- data/lib/lint_fu/{rails → plugins/rails}/unsafe_find_checker.rb +17 -30
- data/lib/lint_fu/plugins/rails.rb +29 -0
- data/lib/lint_fu/plugins.rb +11 -0
- data/lib/lint_fu/scan.rb +1 -49
- data/lib/lint_fu.rb +13 -8
- data/lint_fu.gemspec +10 -7
- metadata +140 -24
- data/lib/lint_fu/action_pack/model_controller.rb +0 -7
- data/lib/lint_fu/action_pack.rb +0 -2
- data/lib/lint_fu/active_record.rb +0 -2
- data/lib/lint_fu/model_element.rb +0 -48
- data/lib/lint_fu/rails/model_application.rb +0 -16
- data/lib/lint_fu/rails/model_application_builder.rb +0 -32
- data/lib/lint_fu/rails.rb +0 -6
data/README.rdoc
CHANGED
@@ -14,7 +14,9 @@ will sometimes report false positives.
|
|
14
14
|
|
15
15
|
= Example
|
16
16
|
|
17
|
-
$
|
17
|
+
$ gem install lint_fu
|
18
|
+
$ cd ~/my_project
|
19
|
+
$ lint_fu
|
18
20
|
|
19
21
|
|
20
|
-
Copyright (c) 2009 Tony Spataro, released under the MIT license
|
22
|
+
Copyright (c) 2009-2011 Tony Spataro, released under the MIT license
|
data/bin/lint_fu
CHANGED
@@ -1,92 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
basedir = File.expand_path(
|
3
|
+
basedir = File.expand_path('../../lib', __FILE__)
|
4
4
|
$LOAD_PATH.unshift(basedir) unless $LOAD_PATH.include?(basedir)
|
5
5
|
|
6
6
|
require 'lint_fu'
|
7
7
|
|
8
|
-
|
9
|
-
print activity, '...'
|
10
|
-
STDOUT.flush
|
11
|
-
t0 = Time.now.to_i
|
12
|
-
yield
|
13
|
-
t1 = Time.now.to_i
|
14
|
-
dt = t1-t0
|
15
|
-
if dt > 0
|
16
|
-
puts "done (#{t1-t0} sec)"
|
17
|
-
STDOUT.flush
|
18
|
-
else
|
19
|
-
puts "done"
|
20
|
-
STDOUT.flush
|
21
|
-
end
|
22
|
-
rescue Exception => e
|
23
|
-
puts 'error!' #ensure we print a newline
|
24
|
-
raise e
|
25
|
-
end
|
26
|
-
|
27
|
-
app_root = File.expand_path('.')
|
28
|
-
|
29
|
-
#Only define the Rake tasks if the plugin loaded successfully
|
30
|
-
@scm = LintFu::SourceControlProvider.for_directory(app_root)
|
31
|
-
|
32
|
-
#Build a model of the application we are scanning.
|
33
|
-
timed("Build a model of the application") do
|
34
|
-
builder = LintFu::Rails::ModelApplicationBuilder.new(app_root)
|
35
|
-
@application = builder.model_elements[0]
|
36
|
-
raise LintFu::ProviderError.new("Unable to identify the source control provider for #{app_root}") unless @scm
|
37
|
-
end
|
38
|
-
|
39
|
-
#Using the model we built, scan the controllers for security bugs.
|
40
|
-
timed("Scan the application") do
|
41
|
-
builder = LintFu::Rails::ScanBuilder.new(app_root)
|
42
|
-
@scan = builder.scan(@application)
|
43
|
-
end
|
44
|
-
|
45
|
-
@genuine_issues = @scan.issues.select { |i| !@scan.blessed?(i) }
|
46
|
-
if @genuine_issues.empty?
|
47
|
-
puts "Clean scan: no issues found. Skipping report."
|
48
|
-
exit(0)
|
49
|
-
end
|
50
|
-
|
51
|
-
#CruiseControl.rb integration: write our report to the CC build artifacts folder
|
52
|
-
output_dir = ENV['CC_BUILD_ARTIFACTS'] || app_root
|
53
|
-
mkdir_p output_dir unless File.directory?(output_dir)
|
54
|
-
|
55
|
-
flavor = ENV['FORMAT'] || 'html'
|
56
|
-
typename = "#{flavor}_report".camelize
|
57
|
-
|
58
|
-
#Use a filename (or STDOUT) for our report that corresponds to its format
|
59
|
-
case flavor
|
60
|
-
when 'html'
|
61
|
-
output_name = File.join(output_dir, 'lint.html')
|
62
|
-
output = File.open(output_name, 'w')
|
63
|
-
when 'text'
|
64
|
-
output = STDOUT
|
65
|
-
else
|
66
|
-
puts "Unrecognized output format #{flavor} (undefined type #{typename})"
|
67
|
-
exit -1
|
68
|
-
end
|
69
|
-
|
70
|
-
klass = LintFu.const_get(typename.to_sym)
|
71
|
-
|
72
|
-
timed("Generate report") do
|
73
|
-
klass.new(@scan, @scm, @genuine_issues).generate(output)
|
74
|
-
output.close
|
75
|
-
end
|
76
|
-
|
77
|
-
#Support automation jobs that need to distinguish between failure due to
|
78
|
-
#broken environment and failure to due issues that were genuinely found by
|
79
|
-
#the lint task.
|
80
|
-
if ENV['STATUS_IF_ISSUES']
|
81
|
-
if(@genuine_issues.size > 0)
|
82
|
-
retval = ENV['STATUS_IF_ISSUES'].to_i
|
83
|
-
else
|
84
|
-
retval = 0
|
85
|
-
end
|
86
|
-
else
|
87
|
-
retval = [@genuine_issues.size, 255].min
|
88
|
-
end
|
89
|
-
|
90
|
-
system("open #{output_name}") if (output != STDOUT && STDOUT.tty?)
|
91
|
-
|
92
|
-
exit( retval )
|
8
|
+
exit( LintFu::CLI.run )
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module LintFu
|
2
|
+
class Blessing
|
3
|
+
VERBOSE_BLESSING_COMMENT = /#\s*(lint|security)\s*[-:]\s*not\s*a?n?\s*([a-z0-9 ]*?) ?(as|because|;)\s*(.*)\s*/i
|
4
|
+
BLESSING_COMMENT = /#\s*(lint|security)\s*[-:]\s*not\s*a?n?\s*([a-z0-9 ]*?)\s*/i
|
5
|
+
|
6
|
+
attr_accessor :issue_class, :sexp, :reason
|
7
|
+
|
8
|
+
def initialize(issue_class, sexp=nil, reason=nil)
|
9
|
+
@issue_class = issue_class
|
10
|
+
@reason = reason
|
11
|
+
end
|
12
|
+
|
13
|
+
def applies_to?(klass)
|
14
|
+
klass = klass.class unless klass.is_a?(Class)
|
15
|
+
|
16
|
+
while klass
|
17
|
+
return true if klass.name.index(self.issue_class)
|
18
|
+
klass = klass.superclass
|
19
|
+
end
|
20
|
+
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse(comments, sexp=nil)
|
25
|
+
comments = [comments] unless comments.kind_of?(Array)
|
26
|
+
blessings = []
|
27
|
+
|
28
|
+
comments.each do |line|
|
29
|
+
match = VERBOSE_BLESSING_COMMENT.match(line)
|
30
|
+
match = BLESSING_COMMENT.match(line) unless match
|
31
|
+
next unless match
|
32
|
+
issue_class = match[2].downcase.split(/\s+/).join('_').camelize
|
33
|
+
reason = match[3]
|
34
|
+
blessings << Blessing.new(issue_class, sexp, reason)
|
35
|
+
end
|
36
|
+
|
37
|
+
return blessings
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/lint_fu/checker.rb
CHANGED
@@ -1,11 +1,57 @@
|
|
1
1
|
module LintFu
|
2
2
|
class Checker
|
3
|
+
SUPPRESSION_COMMENT = /#\s*lint\s*[-:]\s*(suppress|ignore) (.*)/i
|
4
|
+
|
3
5
|
attr_reader :scan, :context, :file
|
4
6
|
|
5
7
|
def initialize(scan, context, file=nil)
|
6
|
-
@scan
|
7
|
-
@context
|
8
|
-
@file
|
8
|
+
@scan = scan
|
9
|
+
@context = context
|
10
|
+
@file = file
|
11
|
+
@suppressed = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# This class responds to any method beginning with "observe_" in order to provide
|
15
|
+
# a default callback for every kind of Sexp it might encounter.
|
16
|
+
def method_missing(meth, *args)
|
17
|
+
return true if meth.to_s =~ /^observe_/
|
18
|
+
super(meth, *args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def observe_class_begin(sexp)
|
22
|
+
enter_suppression_scope(sexp)
|
23
|
+
end
|
24
|
+
|
25
|
+
def observe_class_end(sexp)
|
26
|
+
leave_suppression_scope(sexp)
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def suppressed?(klass)
|
32
|
+
basename = klass.name.split('::').last
|
33
|
+
@suppressed.any? { |s| s.include?(basename) }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def enter_suppression_scope(sexp)
|
39
|
+
set = Set.new
|
40
|
+
|
41
|
+
if (comments = sexp.preceding_comments)
|
42
|
+
comments.each do |line|
|
43
|
+
match = SUPPRESSION_COMMENT.match(line)
|
44
|
+
next unless match
|
45
|
+
list = match[2].split(/\s*(,|and)\s*/)
|
46
|
+
list.each { |s| set << s.gsub(/\s+/, '_').camelize }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@suppressed.push set
|
51
|
+
end
|
52
|
+
|
53
|
+
def leave_suppression_scope(sexp)
|
54
|
+
@suppressed.pop
|
9
55
|
end
|
10
56
|
end
|
11
57
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module LintFu::CLI
|
2
|
+
# Base class for CLI commands
|
3
|
+
class Command
|
4
|
+
def initialize(options)
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def app_root
|
15
|
+
File.expand_path('.')
|
16
|
+
end
|
17
|
+
|
18
|
+
def scm
|
19
|
+
@scm ||= LintFu::SourceControlProvider.for_directory(app_root)
|
20
|
+
raise LintFu::ProviderError.new("Unable to identify the source control provider for #{app_root}") unless @scm
|
21
|
+
@scm
|
22
|
+
end
|
23
|
+
|
24
|
+
def say(*args)
|
25
|
+
LintFu::CLI.say(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def timed(activity)
|
29
|
+
print activity, '...'
|
30
|
+
STDOUT.flush
|
31
|
+
t0 = Time.now.to_i
|
32
|
+
yield
|
33
|
+
t1 = Time.now.to_i
|
34
|
+
dt = t1-t0
|
35
|
+
if dt > 0
|
36
|
+
puts "done (#{t1-t0} sec)"
|
37
|
+
STDOUT.flush
|
38
|
+
else
|
39
|
+
puts "done"
|
40
|
+
STDOUT.flush
|
41
|
+
end
|
42
|
+
rescue Exception => e
|
43
|
+
print 'error!' unless e.is_a?(SignalException)
|
44
|
+
puts
|
45
|
+
raise e
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module LintFu::CLI
|
2
|
+
class Prune < Command
|
3
|
+
RUBY_FILE_EXT = /\.rb[a-z]?/
|
4
|
+
|
5
|
+
def run
|
6
|
+
#Build a model of the application we are scanning.
|
7
|
+
timed("Build a model of the application") do
|
8
|
+
builder = LintFu::Plugins::Rails.context_builder_for(self.app_root)
|
9
|
+
builder.build
|
10
|
+
@application = builder.eide.first
|
11
|
+
end
|
12
|
+
|
13
|
+
#Using the model we built, scan the controllers for security bugs.
|
14
|
+
timed("Scan the application") do
|
15
|
+
@scan = LintFu::Scan.new(self.app_root)
|
16
|
+
#TODO generalize/abstract this, same as we did for context builders
|
17
|
+
builder = LintFu::Plugins::Rails.issue_builder_for(self.app_root)
|
18
|
+
builder.build(@application, @scan)
|
19
|
+
end
|
20
|
+
|
21
|
+
blessings = []
|
22
|
+
blessing_ranges = nil
|
23
|
+
useless = []
|
24
|
+
|
25
|
+
timed("Find all annotations") do
|
26
|
+
recurse(self.app_root, blessings)
|
27
|
+
|
28
|
+
blessing_ranges = blessings.map do |triple|
|
29
|
+
file, line, comment = triple[0], triple[1], triple[2]
|
30
|
+
next LintFu::FileRange.new(file, line, line, comment)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
timed("Cross-check annotations against issues") do
|
35
|
+
issue_ranges = @scan.issues.map do |issue|
|
36
|
+
issue.sexp.preceding_comment_range
|
37
|
+
end
|
38
|
+
issue_ranges.compact!
|
39
|
+
|
40
|
+
blessing_ranges.each do |b|
|
41
|
+
useless << b unless issue_ranges.any? { |r| r.include?(b) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
say "Found #{useless.size} extraneous annotations (out of #{blessings.size} total)."
|
46
|
+
|
47
|
+
useless.each do |range|
|
48
|
+
filename = File.relative_path(self.app_root, range.filename)
|
49
|
+
say "#{filename}:#{range.line}"
|
50
|
+
end
|
51
|
+
|
52
|
+
say "WARNING: I did not actually prune these; you need to do it yourself!!"
|
53
|
+
|
54
|
+
return 0
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def recurse(dir, results)
|
60
|
+
dir = File.expand_path(dir)
|
61
|
+
Dir.glob(File.join(dir, '*')).each do |dirent|
|
62
|
+
if File.directory?(dirent)
|
63
|
+
recurse(dirent, results)
|
64
|
+
elsif dirent =~ RUBY_FILE_EXT
|
65
|
+
find_blessings(dirent, File.readlines(dirent), results)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_blessings(file, lines, results)
|
71
|
+
lines.each_with_index do |line, i|
|
72
|
+
blessing = LintFu::Blessing.parse(line)
|
73
|
+
next if blessing.empty?
|
74
|
+
results << [file, i+1, line]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module LintFu::CLI
|
2
|
+
class Scan < Command
|
3
|
+
def run
|
4
|
+
#Build a model of the application we are scanning.
|
5
|
+
timed("Build a model of the application") do
|
6
|
+
builder = LintFu::Plugins::Rails.context_builder_for(self.app_root)
|
7
|
+
|
8
|
+
unless builder
|
9
|
+
say "Cannot determine context builder for #{File.basename(self.app_root)}."
|
10
|
+
say "Either this application uses a framework that is unsupported by LintFu,"
|
11
|
+
say "or a bug is preventing us from recognizing the application framework."
|
12
|
+
say "Sorry!"
|
13
|
+
exit(-1)
|
14
|
+
end
|
15
|
+
|
16
|
+
builder.build
|
17
|
+
@application = builder.eide.first
|
18
|
+
end
|
19
|
+
|
20
|
+
#Using the model we built, scan the controllers for security bugs.
|
21
|
+
timed("Scan the application") do
|
22
|
+
@scan = LintFu::Scan.new(self.app_root)
|
23
|
+
#TODO generalize/abstract this, same as we did for context builders
|
24
|
+
builder = LintFu::Plugins::Rails.issue_builder_for(self.app_root)
|
25
|
+
builder.build(@application, @scan)
|
26
|
+
end
|
27
|
+
|
28
|
+
@genuine_issues = @scan.issues.select { |i| !@scan.blessed?(i) }
|
29
|
+
if @genuine_issues.empty?
|
30
|
+
say "Clean scan: no issues found. Skipping report."
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
#CruiseControl.rb integration: write our report to the CC build artifacts folder
|
35
|
+
output_dir = ENV['CC_BUILD_ARTIFACTS'] || self.app_root
|
36
|
+
mkdir_p output_dir unless File.directory?(output_dir)
|
37
|
+
|
38
|
+
flavor = ENV['FORMAT'] || 'html'
|
39
|
+
typename = "#{flavor}_report".camelize
|
40
|
+
|
41
|
+
#Use a filename (or STDOUT) for our report that corresponds to its format
|
42
|
+
case flavor
|
43
|
+
when 'html'
|
44
|
+
output_name = File.join(output_dir, 'lint.html')
|
45
|
+
output = File.open(output_name, 'w')
|
46
|
+
when 'text'
|
47
|
+
output = STDOUT
|
48
|
+
else
|
49
|
+
say "Unrecognized output format #{flavor} (undefined type #{typename})"
|
50
|
+
exit -1
|
51
|
+
end
|
52
|
+
|
53
|
+
klass = LintFu.const_get(typename.to_sym)
|
54
|
+
|
55
|
+
timed("Generate report") do
|
56
|
+
klass.new(@scan, self.scm, @genuine_issues).generate(output)
|
57
|
+
output.close
|
58
|
+
end
|
59
|
+
|
60
|
+
#Support automation jobs that need to distinguish between failure due to
|
61
|
+
#broken environment and failure to due issues that were genuinely found by
|
62
|
+
#the lint task.
|
63
|
+
if ENV['STATUS_IF_ISSUES']
|
64
|
+
if(@genuine_issues.size > 0)
|
65
|
+
retval = ENV['STATUS_IF_ISSUES'].to_i
|
66
|
+
else
|
67
|
+
retval = 0
|
68
|
+
end
|
69
|
+
else
|
70
|
+
retval = [@genuine_issues.size, 255].min
|
71
|
+
end
|
72
|
+
|
73
|
+
system("open #{output_name}") if (output != STDOUT && STDOUT.tty?)
|
74
|
+
|
75
|
+
return retval
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/lint_fu/cli.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module LintFu
|
2
|
+
module CLI
|
3
|
+
BANNER = <<EOS
|
4
|
+
Lint-fu finds security defects in Ruby code.
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
lint_fu [options]
|
8
|
+
where [options] are:
|
9
|
+
EOS
|
10
|
+
|
11
|
+
def self.run
|
12
|
+
commands = []
|
13
|
+
ObjectSpace.each_object(Class) do |c|
|
14
|
+
commands << c.to_s.underscore if c.superclass == Command
|
15
|
+
end
|
16
|
+
|
17
|
+
opts = Trollop::options do
|
18
|
+
version "Lint-Fu #{Gem.loaded_specs['lint_fu'].version} (c) 2011 Tony Spataro"
|
19
|
+
banner BANNER
|
20
|
+
stop_on commands
|
21
|
+
end
|
22
|
+
|
23
|
+
cmd_name = ARGV.shift || 'scan'
|
24
|
+
sym = cmd_name.camelize.to_sym
|
25
|
+
|
26
|
+
begin
|
27
|
+
klass = const_get(sym)
|
28
|
+
raise NameError unless klass.superclass == Command
|
29
|
+
rescue NameError => e
|
30
|
+
Trollop::die "Unknown command #{cmd_name}"
|
31
|
+
end
|
32
|
+
|
33
|
+
cmd = klass.new(opts)
|
34
|
+
return cmd.run
|
35
|
+
rescue Interrupt => e
|
36
|
+
say "Interrupt; exiting without completing task."
|
37
|
+
exit(-1)
|
38
|
+
end
|
39
|
+
def self.say(*args)
|
40
|
+
puts(*args)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# The base class (Command) should be loaded first
|
46
|
+
require 'lint_fu/cli/command'
|
47
|
+
|
48
|
+
# Everyone else can be loaded automagically
|
49
|
+
cli_dir = File.expand_path('../cli', __FILE__)
|
50
|
+
Dir[File.join(cli_dir, '*.rb')].each do |file|
|
51
|
+
require file
|
52
|
+
end
|