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