fuzzbert 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Martin Boßlet
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,204 @@
1
+ # FuzzBert [![Build Status](https://secure.travis-ci.org/krypt/FuzzBert.png?branch=master)](http://travis-ci.org/krypt/FuzzBert)
2
+
3
+ A random testing/fuzzer framework for Ruby.
4
+
5
+ Random Testing (or "fuzzing") is not really new, it has been around for quite
6
+ some time. Yet it still hasn't found widespread adoption in everyday coding
7
+ practices, much too often it is only used for the purpose of finding exploits
8
+ for existing applications or libraries. FuzzBert wants to improve this situation.
9
+ It's a simple fuzzing framework with an RSpec-like DSL that will allow you to
10
+ integrate random tests in your project with minimal effort.
11
+
12
+ For further information on random testing, here are two excellent starting points:
13
+
14
+ * [Udacity CS258](http://www.udacity.com/overview/Course/cs258/)
15
+ * [Babysitting an Army of Monkeys](http://fuzzinginfo.files.wordpress.com/2012/05/cmiller-csw-2010.pdf)
16
+
17
+ ## Installation
18
+
19
+ gem install fuzzbert
20
+ fuzzbert --help
21
+
22
+ ## Defining a random test
23
+
24
+ FuzzBert defines an RSpec-like DSL that can be used to define different fuzzing
25
+ scenarios. The DSL uses three words: `fuzz`, `deploy` and `data`. `fuzz` can be
26
+ thought of as defining a new scenario, such as "fuzz this command line tool",
27
+ "fuzz this particular URL of my web app" or "fuzz this library method taking
28
+ external input".
29
+
30
+ Within a `fuzz` block, there must be one occurence of `deploy` and one or several
31
+ occurences of `data`. The `deploy` block is the spot where we deliver the random
32
+ payload that has been generated. It is agnostic about the actual target in order to
33
+ leave you free to fuzz whatever you require in your particular case. The `data`
34
+ blocks define the shape of the random data being generated. There can be more than
35
+ one such block because it is often beneficial to not only shoot completely random
36
+ data at the target - you often want to deliver more structured data as well, trying
37
+ to find the edge cases deeper within your code. Good random test suites make use
38
+ of both - totally random data as well as structured data - in order to cover as
39
+ much "code surface" as possible.
40
+
41
+ Here is a quick example that fuzzes `JSON.parse`:
42
+
43
+ ```ruby
44
+ require 'json'
45
+ require 'fuzzbert'
46
+
47
+ fuzz "JSON.parse" do
48
+
49
+ deploy do |data|
50
+ begin
51
+ JSON.parse data
52
+ rescue StandardError
53
+ #fine, we just want to capture crashes
54
+ end
55
+ end
56
+
57
+ data "completely random" do
58
+ FuzzBert::Generators.random
59
+ end
60
+
61
+ data "enclosing curly braces" do
62
+ c = FuzzBert::Container.new
63
+ c << FuzzBert::Generators.fixed("{")
64
+ c << FuzzBert::Generators.random
65
+ c << FuzzBert::Generators.fixed("}")
66
+ c.generator
67
+ end
68
+
69
+ data "my custom generator" do
70
+ prng = Random.new
71
+ lambda do
72
+ buf = '{ user: { '
73
+ buf << prng.bytes(100)
74
+ buf << ' } }'
75
+ end
76
+ end
77
+
78
+ end
79
+ ```
80
+
81
+ The `deploy` block takes the generated data as a parameter. The block itself is
82
+ responsible of deploying the payload. An execution is considered successful if
83
+ the `deploy` block passes with no uncaught error being raised. If an error slips
84
+ through or if the Ruby process crashes altogether, the execution is of course
85
+ considered as a failure.
86
+
87
+ `data` blocks must return a lambda or proc that takes no argument. You can either
88
+ choose completely custom lambdas of your own or use those predefined for you in
89
+ `FuzzBert::Generators`.
90
+
91
+ ## Templates
92
+
93
+ Using the approach described so far is most useful for binary protocols, but as
94
+ soon as largely String-based data is needed it can soon become tedious. What you
95
+ actually want in these situations is some sort of template mechanism that comes
96
+ with mostly fixed data and only replaces a few selected parts with randomly
97
+ generated data. This, too, is possible with FuzzBert, it comes with a minimal
98
+ templating language:
99
+
100
+ ```ruby
101
+ require 'fuzzbert'
102
+
103
+ fuzz "My Web App" do
104
+
105
+ deploy do |data|
106
+ # Send the data to your web app with httpclient or similar.
107
+ # You define the "error conditions": if a response to some
108
+ # data is not as expected, you could simply raise an error
109
+ # here.
110
+ end
111
+
112
+ data "JSON generated from a template" do
113
+ t = FuzzBert::Template.new '{ user: { id: ${id}, name: "${name}" } }'
114
+ t.set(:id) { FuzzBert::Generators.cycle(1..10000) }
115
+ t.set(:name) { FuzzBert::Generators.random }
116
+ t.generator
117
+ end
118
+
119
+ end
120
+ ```
121
+
122
+ ## Mutators
123
+
124
+ Mutation is the principle used in "Babysitting an Army of Monkeys". The basis for
125
+ the mutation tests is a valid sample of input that is then modified in exactly one
126
+ position in each test instance. You can apply this principle as follows:
127
+
128
+ ```ruby
129
+ require 'fuzzbert'
130
+
131
+ fuzz "Web App" do
132
+ deploy do |data|
133
+ #send JSON data via HTTP
134
+ end
135
+
136
+ data "mutated data" do
137
+ m = FuzzBert::Mutator.new '{ user: { id: 42, name: "FuzzBert" }'
138
+ m.generator
139
+ end
140
+
141
+ end
142
+ ```
143
+
144
+ ## How the tests are performed
145
+
146
+ Each `fuzz` block defines a `TestSuite`. These are executed in a round-robin manner.
147
+ Each individual `TestSuite` will then apply the `deploy` block with a sample of
148
+ data generated successively by each one of the `data` blocks. Once all `data` blocks
149
+ were used, the next `TestSuite` will be executed and so on... By default, a FuzzBert
150
+ fuzzing session runs forever, until the process is either killed or by manually hitting
151
+ `CTRL+C` for example. This was a deliberate design choice since random testing suites
152
+ need to be run for quite some time to be effective. It's something you want to run over
153
+ the weekend rather than for a couple of minutes. Still, it can make sense to explicitly
154
+ limit the number of runs, for example when integrating FuzzBert with a CI server or
155
+ with Travis. You can do so by passing the `--limit` parameter to the `fuzzbert`
156
+ executable.
157
+
158
+ Every single execution of `deploy` is run in a separate process. The main reason for
159
+ this is that we typically want to detect hard crashes when a C extension or even Ruby
160
+ itself encounters an input it can't handle. Besides being able to cope with these cases,
161
+ running in separate processes proves beneficial otherwise as well: by default, FuzzBert
162
+ runs the tests in four separate processes at once, therefore utilizing your CPU's cores
163
+ effectively. You can tweak that setting with `--pool-size` to set this number to 1
164
+ (for completely sequential runs) or to the exact number of cores your CPU offers.
165
+
166
+ ## What happens if we encounter a bug?
167
+
168
+ If a test should end up failing (either the process crashed completely or caused an
169
+ uncaught error), FuzzBert will output the failing test on your terminal and tell you
170
+ where it stored the data that caused this. This conveniently allows you to run FuzzBert
171
+ over the weekend and when you return on Monday, the troubleshooters will sit there all
172
+ lined up for you to go through and filter. By using the `--console` command line switch
173
+ you can tell FuzzBert to not explicitly store the data, but echoing the data that
174
+ caused the crash to the terminal instead.
175
+
176
+ ## Rake integration
177
+
178
+ You may integrate Rake tasks for FuzzBert similar to how you would include a task for
179
+ Rspec:
180
+
181
+ ```ruby
182
+ require 'rake'
183
+ require 'fuzzbert/rake_task'
184
+
185
+ FuzzBert::RakeTask.new(:fuzz) do |spec|
186
+ spec.fuzzbert_opts = ['--limit 10000000', '--console']
187
+ spec.pattern = 'fuzz/**/fuzz_*.rb'
188
+ end
189
+ ```
190
+
191
+ ## Supported versions
192
+
193
+ FuzzBert has been confirmed to run on CRuby 1.9.3 and Rubinius 2.0.0dev. Since
194
+ it heavily relies on forking, it does not run on JRuby so far, but support is planned and
195
+ on its way.
196
+
197
+ You may also use FuzzBert for fuzzing arbitrary applications or libraries that aren't
198
+ connected to Ruby at all - have a look in the examples that ship with FuzzBert.
199
+
200
+ ## License
201
+
202
+ Copyright (c) 2012 Martin Boßlet. Distributed under the MIT License. See LICENSE for
203
+ further details.
204
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'fuzzbert'
5
+ FuzzBert::AutoRun.autorun
6
+ rescue LoadError
7
+ $stderr.puts "Could not find fuzzbert."
8
+ exit(1)
9
+ end
@@ -0,0 +1,36 @@
1
+ =begin
2
+
3
+ = Info
4
+
5
+ FuzzBert - Random Testing / Fuzzing in Ruby
6
+
7
+ Copyright (C) 2012
8
+ Martin Bosslet <martin.bosslet@googlemail.com>
9
+ All rights reserved.
10
+
11
+ = License
12
+
13
+ See the file 'LICENSE' for further details.
14
+
15
+ =end
16
+
17
+ module FuzzBert
18
+
19
+ PRNG = Random.new
20
+
21
+ end
22
+
23
+ require_relative 'fuzzbert/version'
24
+ require_relative 'fuzzbert/generation'
25
+ require_relative 'fuzzbert/generators'
26
+ require_relative 'fuzzbert/generator'
27
+ require_relative 'fuzzbert/mutator'
28
+ require_relative 'fuzzbert/template'
29
+ require_relative 'fuzzbert/container'
30
+ require_relative 'fuzzbert/test'
31
+ require_relative 'fuzzbert/error_handler'
32
+ require_relative 'fuzzbert/test_suite'
33
+ require_relative 'fuzzbert/executor'
34
+ require_relative 'fuzzbert/autorun'
35
+ require_relative 'fuzzbert/dsl'
36
+
@@ -0,0 +1,65 @@
1
+ require 'optparse'
2
+
3
+ module FuzzBert::AutoRun
4
+
5
+ TEST_CASES = []
6
+
7
+ module_function
8
+
9
+ def register(suite)
10
+ TEST_CASES << suite
11
+ end
12
+
13
+ def autorun
14
+ options, files = process_args(ARGV)
15
+ load_files(files)
16
+ run(options)
17
+ end
18
+
19
+ def run(options=nil)
20
+ executor = options ? FuzzBert::Executor.new(TEST_CASES, options) : FuzzBert::Executor.new(TEST_CASES)
21
+ executor.run
22
+ end
23
+
24
+ private; module_function
25
+
26
+ def load_files(files)
27
+ files.each do |pattern|
28
+ Dir.glob(pattern).each { |f| load File.expand_path(f) }
29
+ end
30
+ end
31
+
32
+ def process_args(args = [])
33
+ options = {}
34
+ orig_args = args.dup
35
+
36
+ OptionParser.new do |opts|
37
+ opts.banner = 'FuzzBert options:'
38
+ opts.version = FuzzBert::VERSION
39
+
40
+ opts.on '-h', '--help', 'Display this help.' do
41
+ puts opts
42
+ exit
43
+ end
44
+
45
+ opts.on '--pool-size SIZE', Integer, "Sets the number of concurrently running processes to SIZE" do |n|
46
+ options[:pool_size] = n.to_i
47
+ end
48
+
49
+ opts.on '--limit LIMIT', Integer, "Instead of running permanently, fuzzing will be stopped after running LIMIT of instances" do |n|
50
+ p n
51
+ options[:limit] = n.to_i
52
+ end
53
+
54
+ opts.on '--console', "Output the failing cases including data on the console instead of saving them in a file" do
55
+ options[:handler] = FuzzBert::Handler::Console.new
56
+ end
57
+
58
+ opts.parse! args
59
+ end
60
+
61
+ [options, args]
62
+ end
63
+
64
+ end
65
+
@@ -0,0 +1,33 @@
1
+
2
+ module FuzzBert::AutoRun
3
+
4
+ TEST_CASES = []
5
+
6
+ module_function
7
+
8
+ def register(suite)
9
+ TEST_CASES << suite
10
+ end
11
+
12
+ def autorun
13
+ at_exit do
14
+ next if $! # don't run if there was an exception
15
+
16
+ load_files(ARGV)
17
+ run
18
+
19
+ end unless @@installed_at_exit
20
+ @@installed_at_exit = true
21
+ end
22
+
23
+ def run
24
+ FuzzBert::Executor.new(TEST_CASES).run
25
+ end
26
+
27
+ private
28
+
29
+ def load_files(args)
30
+ end
31
+
32
+ end
33
+
@@ -0,0 +1,21 @@
1
+
2
+ class FuzzBert::Container
3
+ include FuzzBert::Generation
4
+
5
+ def initialize(generators=[])
6
+ @generators = generators
7
+ end
8
+
9
+ def <<(generator)
10
+ @generators << generator
11
+ end
12
+
13
+ def to_data
14
+ "".tap do |buf|
15
+ @generators.each { |gen| buf << gen.call }
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+
@@ -0,0 +1,9 @@
1
+ module FuzzBert::DSL
2
+ def fuzz(*args, &blk)
3
+ suite = FuzzBert::TestSuite.create(*args, &blk)
4
+ FuzzBert::AutoRun.register(suite)
5
+ end
6
+ end
7
+
8
+ extend FuzzBert::DSL
9
+ Module.send(:include, FuzzBert::DSL)
@@ -0,0 +1,19 @@
1
+
2
+ module FuzzBert::Handler
3
+ class FileOutput
4
+ def handle(id, data, pid, status)
5
+ prefix = status.termsig ? "crash" : "bug"
6
+ filename = "#{prefix}#{pid}"
7
+ File.open(filename, "wb") { |f| f.print(data) }
8
+ puts "#{id} failed. Data was saved as #{filename}."
9
+ end
10
+ end
11
+
12
+ class Console
13
+ def handle(id, data, pid, status)
14
+ puts "#{id} failed. Data: #{data.inspect}"
15
+ end
16
+ end
17
+ end
18
+
19
+
@@ -0,0 +1,153 @@
1
+ class FuzzBert::Executor
2
+
3
+ attr_reader :pool_size, :limit, :handler
4
+
5
+ DEFAULT_POOL_SIZE = 4
6
+ DEFAULT_LIMIT = -1
7
+ DEFAULT_HANDLER = FuzzBert::Handler::FileOutput
8
+
9
+ def initialize(suites, args = {
10
+ pool_size: DEFAULT_POOL_SIZE,
11
+ limit: DEFAULT_LIMIT,
12
+ handler: DEFAULT_HANDLER.new
13
+ })
14
+ @pool_size = args[:pool_size] || DEFAULT_POOL_SIZE
15
+ @limit = args[:limit] || DEFAULT_LIMIT
16
+ @handler = args[:handler] || DEFAULT_HANDLER.new
17
+ @data_cache = {}
18
+ @n = 0
19
+ @exiting = false
20
+ @producer = DataProducer.new(suites)
21
+ end
22
+
23
+ def run
24
+ trap_child_exit
25
+ trap_interrupt
26
+
27
+ @pool_size.times { run_instance(*@producer.next) }
28
+ @running = true
29
+ @limit == -1 ? sleep : conditional_sleep
30
+ end
31
+
32
+ private
33
+
34
+ def run_instance(description, test, generator)
35
+ data = generator.to_data
36
+ pid = fork do
37
+ begin
38
+ test.run(data)
39
+ rescue StandardError
40
+ abort
41
+ end
42
+ end
43
+ id = "#{description}/#{generator.description}"
44
+ @data_cache[pid] = [id, data]
45
+ end
46
+
47
+ def trap_child_exit
48
+ trap(:CHLD) do
49
+ begin
50
+ while exitval = Process.wait2(-1, Process::WNOHANG)
51
+ pid = exitval[0]
52
+ status = exitval[1]
53
+ data_ary = @data_cache.delete(pid)
54
+ unless status.success?
55
+ handle(data_ary[0], data_ary[1], pid, status) unless interrupted(status)
56
+ end
57
+ @n += 1
58
+ if @limit == -1 || @n < @limit
59
+ run_instance(*@producer.next)
60
+ else
61
+ @running = false
62
+ end
63
+ end
64
+ rescue Errno::ECHILD
65
+ end
66
+ end
67
+ end
68
+
69
+ def trap_interrupt
70
+ trap(:INT) do
71
+ exit! (1) if @exiting
72
+ @exiting = true
73
+ graceful_exit
74
+ end
75
+ end
76
+
77
+ def graceful_exit
78
+ puts "\nExiting...Interrupt again to exit immediately"
79
+ begin
80
+ while Process.wait; end
81
+ rescue Errno::ECHILD
82
+ end
83
+ exit 0
84
+ end
85
+
86
+ def handle(id, data, pid, status)
87
+ @handler.handle(id, data, pid, status)
88
+ end
89
+
90
+ def interrupted(status)
91
+ return false if status.exited?
92
+ return true if status.termsig == nil || status.termsig == 2
93
+ end
94
+
95
+ def conditional_sleep
96
+ sleep 0.1 until @running == false
97
+ end
98
+
99
+ class DataProducer
100
+ def initialize(suites)
101
+ @ring = Ring.new(suites)
102
+ update
103
+ end
104
+
105
+ def update
106
+ @suite = @ring.next
107
+ @gen_iter = ProcessSafeEnumerator.new(@suite.generators)
108
+ end
109
+
110
+ def next
111
+ gen = nil
112
+ until gen
113
+ begin
114
+ gen = @gen_iter.next
115
+ rescue StopIteration
116
+ update
117
+ end
118
+ end
119
+ [@suite.description, @suite.test, gen]
120
+ end
121
+
122
+ class Ring
123
+ def initialize(objs)
124
+ @i = 0
125
+ objs = [objs] unless objs.respond_to?(:each)
126
+ @objs = objs.to_a
127
+ end
128
+
129
+ def next
130
+ obj = @objs[@i]
131
+ @i = (@i + 1) % @objs.size
132
+ obj
133
+ end
134
+ end
135
+
136
+ #needed because the Fiber used for normal Enumerators has race conditions
137
+ class ProcessSafeEnumerator
138
+ def initialize(ary)
139
+ @i = 0
140
+ @ary = ary.to_a
141
+ end
142
+
143
+ def next
144
+ obj = @ary[@i]
145
+ raise StopIteration unless obj
146
+ @i += 1
147
+ obj
148
+ end
149
+ end
150
+ end
151
+
152
+ end
153
+