├── .travis.yml ├── LICENSE.txt ├── README.md ├── bin └── fuzz-get ├── index.js ├── package.json └── test ├── data ├── fc.json └── string.json └── mutate.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/mapbox/fuzzer.svg?branch=master)](https://travis-ci.org/mapbox/fuzzer) 2 | 3 | # fuzzer 4 | 5 | A [fuzzer](http://en.wikipedia.org/wiki/Fuzz_testing) for testing. This implements 6 | mutation fuzzing, in which an expect input is mutated (changed) many times 7 | in order to trigger unexpected behavior or crashes. 8 | 9 | ## install 10 | 11 | npm install fuzzer 12 | 13 | ## api 14 | 15 | ### fuzzer.mutate.object(obj) 16 | 17 | Generate a mutated version of an object. This does not modify the object 18 | directly, but returns a modified copy. This mutation will increment and 19 | decrement numbers, randomize arrays, remove properties, and more. 20 | 21 | ### fuzzer.mutate.string(str) 22 | 23 | Generate a mutated version of a string, with reversed, removed, and added 24 | characters. 25 | 26 | ### fuzzer.seed(number) 27 | 28 | Seed the random number generator `random-js` that determines mutations. 29 | By calling this function with the same number, you can generate the same 30 | mutations consistently. 31 | 32 | ## fuzz-get 33 | 34 | If you install this globally it provides a single cli utility called `fuzz-get`. 35 | 36 | npm install -g fuzzer 37 | fuzz-get "./bin/fuzz-get "http://localhost:8889/foo/bar/your/rest/api" 38 | 39 | This will run mutated requests against your server continously - it will mutate the 40 | path requested into other incorrect requests, and log in the form: 41 | 42 | HTTP200:/foo/bar/your/rest/api 43 | HTTP404:/foo/baryour/rest/api 44 | 45 | So you can pipe into `| grep "HTTP500"` if you wish. 46 | 47 | ## example 48 | 49 | ```js 50 | var test = require('tap').test, 51 | fuzzer = require('fuzzer'); 52 | 53 | fuzzer.seed(0); 54 | 55 | test('something', function(t) { 56 | var generator = fuzzer.mutate.object(yourTestingInput); 57 | for (var i = 0; i < 1000; i++) { 58 | t.doesNotThrow(function() { 59 | yourLibrary(generator()); 60 | }); 61 | } 62 | }); 63 | ``` 64 | -------------------------------------------------------------------------------- /bin/fuzz-get: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var http = require('http'), 4 | url = require('url'), 5 | fuzzer = require('../'), 6 | xtend = require('xtend'); 7 | 8 | var input = process.argv[2], 9 | parsed = url.parse(input); 10 | 11 | function run() { 12 | var mutatedPath = fuzzer.mutate.string(parsed.path); 13 | if (mutatedPath[0] !== '/') mutatedPath = '/' + mutatedPath; 14 | if (mutatedPath === parsed.path) { 15 | setTimeout(run, 10); 16 | } 17 | else { 18 | var mutated = xtend(parsed, { 19 | path: mutatedPath 20 | }); 21 | http.get(mutated, function(res) { 22 | console.log('HTTP' + res.statusCode + ':' + mutatedPath); 23 | res.on('end', function() { }); 24 | res.on('data', function(e) { }); 25 | res.on('error', function(e) { }); 26 | setTimeout(run, 10); 27 | }); 28 | } 29 | } 30 | 31 | run(); 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Random = require('random-js'), 2 | xtend = require('xtend'), 3 | traverse = require('traverse'); 4 | 5 | var random = new Random(Random.engines.mt19937().seed(0)); 6 | 7 | /** 8 | * @param _ {number} 9 | * @returns {null} 10 | */ 11 | module.exports.seed = function(_) { 12 | random = new Random(Random.engines.mt19937().seed(_)); 13 | }; 14 | 15 | module.exports.mutate = { 16 | object: mutateObject, 17 | string: mutateString 18 | }; 19 | 20 | function mutateObject(obj) { 21 | return function generate() { 22 | // copy the object so that modifications 23 | // are not additive 24 | var copy = xtend({}, obj); 25 | traverse(copy).forEach(transformObjectValue); 26 | return copy; 27 | }; 28 | } 29 | 30 | function transformObjectValue(val) { 31 | if (random.bool(0.1)) { 32 | mutateVal.call(this, val); 33 | } else if (random.bool(0.05)) { 34 | if (this.level) { 35 | this.remove(); 36 | } 37 | } 38 | } 39 | 40 | function mutateVal(val) { 41 | switch(typeof val) { 42 | case 'boolean': 43 | this.update(!val); 44 | break; 45 | case 'number': 46 | this.update(val + random.real(-1000, 1000)); 47 | break; 48 | case 'string': 49 | this.update(mutateString(val)); 50 | break; 51 | default: 52 | if (Array.isArray(val)) this.update(mutateArray(val)); 53 | break; 54 | } 55 | } 56 | 57 | /** 58 | * @param val {string} a string value 59 | * @returns {string} 60 | */ 61 | function mutateString(val) { 62 | var arr = val.split(''); 63 | if (random.bool(0.05)) { 64 | arr = arr.reverse(); 65 | } 66 | if (random.bool(0.25)) { 67 | arr.splice( 68 | random.integer(1, arr.length), 69 | random.integer(1, arr.length)); 70 | } 71 | if (random.bool(0.25)) { 72 | var args = [random.integer(1, arr.length), 0] 73 | .concat(random.string(random.integer(1, arr.length)).split('')); 74 | arr.splice.apply(arr, args); 75 | } 76 | val = arr.join(''); 77 | return val; 78 | } 79 | 80 | /** 81 | * @param val {array} an array 82 | * @returns {array} 83 | */ 84 | function mutateArray(val) { 85 | if (random.bool(0.1)) { 86 | val = val.reverse(); 87 | } 88 | if (random.bool(0.05)) { 89 | val = val.slice(random.integer(0, val.length)); 90 | } 91 | if (random.bool(0.05)) { 92 | val = val.slice(0, random.integer(0, val.length)); 93 | } 94 | return val; 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuzzer", 3 | "version": "0.2.1", 4 | "description": "a fuzzy input creator for tests", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "bin": { 10 | "fuzz-get": "./bin/fuzz-get" 11 | }, 12 | "dependencies": { 13 | "random-js": "1.0.2", 14 | "traverse": "~0.6.6", 15 | "xtend": "~2.2.0" 16 | }, 17 | "devDependencies": { 18 | "tap": "~0.4.8" 19 | }, 20 | "scripts": { 21 | "test": "tap test/*.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:mapbox/fuzzer.git" 26 | }, 27 | "keywords": [ 28 | "fuzzer", 29 | "fuzzy", 30 | "testing", 31 | "mutation" 32 | ], 33 | "author": "Tom MacWright", 34 | "license": "ISC", 35 | "bugs": { 36 | "url": "https://github.com/mapbox/fuzzer/issues" 37 | }, 38 | "homepage": "https://github.com/mapbox/fuzzer" 39 | } 40 | -------------------------------------------------------------------------------- /test/data/fc.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "type": "FeatureCollection", 4 | "features": [] 5 | }, 6 | "output": [ 7 | { 8 | "type": "FeatureCollection", 9 | "features": [] 10 | }, 11 | { 12 | "type": "FeatureCollection", 13 | "features": [] 14 | }, 15 | { 16 | "type": "FeatureCollection", 17 | "features": [] 18 | }, 19 | { 20 | "type": "FeatureCollection", 21 | "features": [] 22 | }, 23 | { 24 | "type": "FeatureCollection" 25 | }, 26 | { 27 | "type": "FeatureCollection" 28 | }, 29 | { 30 | "type": "FeatureCollection", 31 | "features": [] 32 | }, 33 | { 34 | "type": "FeatureCollection", 35 | "features": [] 36 | }, 37 | { 38 | "type": "FeatureCollection", 39 | "features": [] 40 | }, 41 | { 42 | "type": "FeatureCollection", 43 | "features": [] 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /test/data/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": "hello", 3 | "output": [ 4 | "oll", 5 | "hello", 6 | "hello", 7 | "heo", 8 | "hello", 9 | "helROll", 10 | "hello", 11 | "hellZ4o", 12 | "hellocdEIR", 13 | "helo" 14 | ] 15 | } -------------------------------------------------------------------------------- /test/mutate.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test, 2 | fs = require('fs'), 3 | fuzzer = require('../'); 4 | 5 | var REGENERATE = false; 6 | 7 | test('mutate object', function(t) { 8 | fuzzer.seed(0); 9 | var mutator = fuzzer.mutate.object({ 10 | name: 'Tom', 11 | height: 72 12 | }); 13 | t.deepEqual(mutator(), { 14 | name: "Tom", 15 | height: 72 16 | }); 17 | t.deepEqual(mutator(), { 18 | name: "Tom", 19 | height: 72 20 | }); 21 | t.end(); 22 | }); 23 | 24 | test('fc', function(t) { 25 | var inputObject = JSON.parse(fs.readFileSync(__dirname + '/data/fc.json')); 26 | var fc = fuzzer.mutate.object(inputObject.input); 27 | var outputs = []; 28 | for (var i = 0; i < 10; i++) { 29 | outputs.push(fc()); 30 | } 31 | t.deepEqual(inputObject.output, outputs, 'generates correct output'); 32 | if (REGENERATE) { 33 | inputObject.output = outputs; 34 | fs.writeFileSync(__dirname + '/data/fc.json', JSON.stringify(inputObject, null, 4)); 35 | } 36 | t.end(); 37 | }); 38 | 39 | test('mutate string', function(t) { 40 | var inputObject = JSON.parse(fs.readFileSync(__dirname + '/data/string.json')); 41 | var outputs = []; 42 | for (var i = 0; i < 10; i++) { 43 | outputs.push(fuzzer.mutate.string(inputObject.input)); 44 | } 45 | t.deepEqual(inputObject.output, outputs, 'generates correct output'); 46 | if (REGENERATE) { 47 | inputObject.output = outputs; 48 | fs.writeFileSync(__dirname + '/data/string.json', JSON.stringify(inputObject, null, 4)); 49 | } 50 | t.end(); 51 | }); 52 | --------------------------------------------------------------------------------