lint_fu 0.5.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.rdoc +4 -2
  2. data/bin/lint_fu +2 -86
  3. data/lib/lint_fu/blessing.rb +40 -0
  4. data/lib/lint_fu/checker.rb +49 -3
  5. data/lib/lint_fu/cli/command.rb +48 -0
  6. data/lib/lint_fu/cli/prune.rb +78 -0
  7. data/lib/lint_fu/cli/scan.rb +78 -0
  8. data/lib/lint_fu/cli.rb +52 -0
  9. data/lib/lint_fu/eidos.rb +35 -0
  10. data/lib/lint_fu/{model_element_builder.rb → eidos_builder.rb} +8 -3
  11. data/lib/lint_fu/eidos_container.rb +25 -0
  12. data/lib/lint_fu/file_range.rb +43 -0
  13. data/lib/lint_fu/mixins/sexp_instance_methods.rb +57 -9
  14. data/lib/lint_fu/parser.rb +1 -2
  15. data/lib/lint_fu/plugins/action_pack/controller_eidos.rb +7 -0
  16. data/lib/lint_fu/{action_pack/model_controller_builder.rb → plugins/action_pack/controller_eidos_builder.rb} +6 -6
  17. data/lib/lint_fu/plugins/action_pack.rb +2 -0
  18. data/lib/lint_fu/{active_record/model_model.rb → plugins/active_record/model_eidos.rb} +3 -3
  19. data/lib/lint_fu/{active_record/model_model_builder.rb → plugins/active_record/model_eidos_builder.rb} +17 -11
  20. data/lib/lint_fu/plugins/active_record.rb +2 -0
  21. data/lib/lint_fu/{rails → plugins/rails}/buggy_eager_load_checker.rb +6 -5
  22. data/lib/lint_fu/{rails/scan_builder.rb → plugins/rails/issue_builder.rb} +9 -16
  23. data/lib/lint_fu/plugins/rails/model_application.rb +21 -0
  24. data/lib/lint_fu/plugins/rails/model_application_factory.rb +31 -0
  25. data/lib/lint_fu/{rails → plugins/rails}/sql_injection_checker.rb +9 -5
  26. data/lib/lint_fu/{rails → plugins/rails}/unsafe_find_checker.rb +17 -30
  27. data/lib/lint_fu/plugins/rails.rb +29 -0
  28. data/lib/lint_fu/plugins.rb +11 -0
  29. data/lib/lint_fu/scan.rb +1 -49
  30. data/lib/lint_fu.rb +13 -8
  31. data/lint_fu.gemspec +10 -7
  32. metadata +140 -24
  33. data/lib/lint_fu/action_pack/model_controller.rb +0 -7
  34. data/lib/lint_fu/action_pack.rb +0 -2
  35. data/lib/lint_fu/active_record.rb +0 -2
  36. data/lib/lint_fu/model_element.rb +0 -48
  37. data/lib/lint_fu/rails/model_application.rb +0 -16
  38. data/lib/lint_fu/rails/model_application_builder.rb +0 -32
  39. 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
- $ rake lint
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(File.join(File.dirname(__FILE__), '..', 'lib'))
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
- def timed(activity)
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
@@ -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 = scan
7
- @context = context
8
- @file = 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
@@ -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