fuzzbert 1.0.0

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/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
+