Giter Site home page Giter Site logo

fuzzitdev / jsfuzz Goto Github PK

View Code? Open in Web Editor NEW
594.0 14.0 48.0 158 KB

coverage guided fuzz testing for javascript

Home Page: https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz

License: Apache License 2.0

JavaScript 4.19% TypeScript 95.81%
fuzzing javascript testing fuzz-testing fuzzer

jsfuzz's Introduction

fuzzit.dev was acquired by GitLab and the new home for this repo is here

Jsfuzz: coverage-guided fuzz testing for Javascript

Jsfuzz is coverage-guided fuzzer for testing javascript/nodejs packages.

Fuzzing for safe languages like nodejs is a powerful strategy for finding bugs like unhandled exceptions, logic bugs, security bugs that arise from both logic bugs and Denial-of-Service caused by hangs and excessive memory usage.

Fuzzing can be seen as a powerful and efficient strategy in real-world software in addition to classic unit-tests.

Usage

Fuzz Target

The first step is to implement the following function (also called a fuzz target):

function fuzz(buf) {
  // call your package with buf  
}
module.exports = {
    fuzz
};

Features of the fuzz target:

  • Jsfuzz will call the fuzz target in an infinite loop with random data (according to the coverage guided algorithm) passed to buf( in a separate process).
  • The function must catch and ignore any expected exceptions that arise when passing invalid input to the tested package.
  • The fuzz target must call the test function/library with with the passed buffer or a transformation on the test buffer if the structure is different or from different type.
  • Fuzz functions can also implement application level checks to catch application/logical bugs - For example: decode the buffer with the testable library, encode it again, and check that both results are equal. To communicate the results the result/bug the function should throw an exception.
  • jsfuzz will report any unhandled exceptions as crashes as well as inputs that hit the memory limit specified to jsfuzz or hangs/they run more the the specified timeout limit per testcase.

Here is an example of a simple fuzz function for jpeg-js module.

const jpeg = require('jpeg-js');

function fuzz(buf) {
    try {
        jpeg.decode(buf);
    } catch (e) {
        // Those are "valid" exceptions. we can't catch them in one line as
        // jpeg-js doesn't export/inherit from one exception class/style.
        if (e.message.indexOf('JPEG') !== -1 ||
            e.message.indexOf('length octect') !== -1 ||
            e.message.indexOf('Failed to') !== -1 ||
            e.message.indexOf('DecoderBuffer') !== -1 ||
            e.message.indexOf('invalid table spec') !== -1 ||
            e.message.indexOf('SOI not found') !== -1) {
        } else {
            throw e;
        }
    }
}

module.exports = {
    fuzz
};

Running

The next step is to download js-fuzz and then run your fuzzer

npm i -g jsfuzz
jsfuzz ./examples/jpeg/fuzz.js corpus

# Output:
#0 READ units: 0
#1 NEW     cov: 61 corp: 0 exec/s: 1 rss: 23.37 MB
#23320 PULSE     cov: 61 corp: 1 exec/s: 10614 rss: 35.3 MB
#96022 NEW     cov: 70 corp: 1 exec/s: 11320 rss: 129.95 MB
#96971 NEW     cov: 78 corp: 2 exec/s: 10784 rss: 129.95 MB
#97046 NEW     cov: 79 corp: 3 exec/s: 9375 rss: 129.95 MB
#97081 NEW     cov: 81 corp: 4 exec/s: 11666 rss: 129.95 MB
#97195 NEW     cov: 93 corp: 5 exec/s: 9500 rss: 129.95 MB
#97216 NEW     cov: 97 corp: 6 exec/s: 10500 rss: 129.95 MB
#97238 NEW     cov: 102 corp: 7 exec/s: 11000 rss: 129.95 MB
#97303 NEW     cov: 108 corp: 8 exec/s: 10833 rss: 129.96 MB
#97857 PULSE     cov: 108 corp: 9 exec/s: 225 rss: 129.96 MB
#97857 PULSE     cov: 108 corp: 9 exec/s: 0 rss: 940.97 MB
#97857 PULSE     cov: 108 corp: 9 exec/s: 0 rss: 1566.01 MB
#97857 PULSE     cov: 108 corp: 9 exec/s: 0 rss: 2053.49 MB
MEMORY OOM: exceeded 2048 MB. Killing worker
Worker killed
crash was written to crash-819587841e3c275338593b0d195b6163d5208866870e2abf3be8cfc781d2688d
crash(hex)=ffd8ffc09dfdb0ffff0e5296bd7fbbc4f9579096bd7fbbfc0e80d50000ffff36fa400100236701bf73ffaf8003a57f097f5e000000008023c4f9579096bd7fbb008000001500b34e8c018fda5212

This example quickly finds an infinite hang which takes all the memory in jpeg-js.

Corpus

Jsfuzz will generate and test various inputs in an infinite loop. corpus is optional directory and will be used to save the generated testcases so later runs can be started from the same point and provided as seed corpus.

JsFuzz can also start with an empty directory (i.e no seed corpus) though some valid test-cases in the seed corpus may speed up the fuzzing substantially.

jsfuzz tries to mimic some of the arguments and output style from libFuzzer.

More fuzz targets examples (for real and popular libraries) are located under the examples directory and bugs that were found using those targets are listed in the trophies section.

Coverage

Coverage in Istanbul/NYC format is written to .nyc_output/out.json It can be viewer with nyc cli. For example:

nyc report --reporter=html --exclude-node-modules=false

This will save the html report to coverage directory

Other languages

Currently this library is also ported to python via pythonfuzz

Credits & Acknowledgments

jsfuzz logic is heavily based on go-fuzz originally developed by Dmitry Vyukov's. Which is in turn heavily based on Michal Zalewski AFL.

A previous take on that was done by https://github.com/connor4312/js-fuzz with a bit different design, coverage and interface but it looks like it is currently unmaintained.

For coverage jsfuzz is using istanbuljs instrumentation and coverage library.

Contributions

Contributions are welcome!:) There are still a lot of things to improve, and tests and features to add. We will slowly post those in the issues section. Before doing any major contribution please open an issue so we can discuss and help guide the process before any unnecessary work is done.

Trophies

Feel free to add bugs that you found with jsfuzz to this list via pull-request

jsfuzz's People

Contributors

bookmoons avatar kinow avatar rhysd avatar swapgs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jsfuzz's Issues

ReferenceError: mode is not defined

I have a pretty basic fuzzer and when I try to run it, I have this error

/home/nasa/core-js/fuzzers/mesgfuzz.js:1
var cov_2mys8hqaoz=function(){var path="/home/nasa/core-js/fuzzers/mesgfuzz.js";var hash="c7226559dfd6d640358a29d7b757fc0622e37524";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path:"/home/nasa/core-js/fuzzers/mesgfuzz.js",statementMap:{"0":{start:{line:1,column:15},end:{line:1,column:40}},"1":{start:{line:4,column:4},end:{line:12,column:5}},"2":{start:{line:5,column:23},end:{line:7,column:22}},"3":{start:{line:9,column:8},end:{line:9,column:42}},"4":{start:{line:11,column:8},end:{line:11,column:15}},"5":{start:{line:16,column:0},end:{line:16,column:23}}},fnMap:{"0":{name:"fuzz",decl:{start:{line:3,column:15},end:{line:3,column:19}},loc:{start:{line:3,column:24},end:{line:13,column:1}},line:3}},branchMap:{},s:{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0},f:{"0":0},b:{},_coverageSchema:"43e27e138ebf9cfc5966b082cf9a028302ed4184",hash:"c7226559dfd6d640358a29d7b757fc0622e37524"};var coverage=global[gcv]||(global[gcv]={});if(coverage[path]&&co

ReferenceError: mode is not defined
    at Object.<anonymous> (/home/nasa/core-js/fuzzers/mesgfuzz.js:1:1390)
    at Module._compile (internal/modules/cjs/loader.js:945:30)
    at Module.replacementCompile (/usr/lib/node_modules/jsfuzz/node_modules/append-transform/index.js:58:13)
    at Module._extensions..js (internal/modules/cjs/loader.js:962:10)
    at Object.<anonymous> (/usr/lib/node_modules/jsfuzz/node_modules/append-transform/index.js:62:4)
    at Module.load (internal/modules/cjs/loader.js:798:32)
    at Function.Module._load (internal/modules/cjs/loader.js:711:12)
    at Module.require (internal/modules/cjs/loader.js:838:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at Object.<anonymous> (/usr/lib/node_modules/jsfuzz/build/src/worker.js:74:22)

Any idea what is it about?

Is this missing quote intentional?

Looking at the readme, it has one odd line:

            e.message.indexOf('SOI not found ) !== -1')) {

The single quote is in odd place, is it intentional?

Allocation failed - JavaScript heap out of memory (tokenize)

I encountered a core dump while fuzzing that traces back to jsfuzz's versifier.js:413

[6242:0x34faa20]    18540 ms: Mark-sweep 1291.7 (1426.7) -> 1291.7 (1427.2) MB, 15.0 / 0.0 ms  (average mu = 0.244, current mu = 0.169) allocation failure GC in old space requested
[6242:0x34faa20]    18549 ms: Mark-sweep 1292.4 (1427.2) -> 1292.1 (1427.2) MB, 7.8 / 0.1 ms  (average mu = 0.212, current mu = 0.153) allocation failure GC in old space requested


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x16ebcc0dbe1d]
Security context: 0x147d4f69e6e9 <JSObject>
    1: tokenize(aka tokenize) [0x14d370af2101] [/home/me/.nvm/versions/node/v10.16.3/lib/node_modules/jsfuzz/build/src/versifier.js:~413] [pc=0x16ebcc0ea74d](this=0x35c17a8826f1 <undefined>,data=0x1bc2ece351d1 <Uint8Array map = 0xc82d755b89>)
    2: BuildVerse [0x14d370af1e01] [/home/me/.nvm/versions/node/v10.16.3/lib/node_modules/jsfuzz/build/src/...

It also generated a corpus file of ~144K, while the average corpus size was ~20 bytes

jsfuzz - 1.0.10
node - 10.16.3

exec/s eventually goes to 0, jsfuzz seems to stop working

Really cool tool! I tried it out on a few libs today, and in one case the test run eventually just seemed to stop doing anything. It looks like jsfuzz is still working, but doing nothing (exec/s goes to 0 and stays there). Am I doing something wrong here, or is jsfuzz?

Here's my test case for the rss-parser module:

const Parser = require('rss-parser');
const parser = new Parser();

async function fuzz(buf) {
    try {
      await parser.parseString(buf);
    } catch (e) {
      if (
        e.message.indexOf('Non-whitespace before first tag') !== -1 ||
        e.message.indexOf('Unable to parse XML') !== 1
      ) {
        // ignore
      } else {
        throw e;
      }
    }
}

module.exports = {
    fuzz
};

And here's what I see when I run it. It just keeps going forever on #56120 PULSE cov: 2618 corp: 55 exec/s: 0.

> [email protected] test /private/tmp/rss-parser-fuzz
> jsfuzz fuzz.js

#0 READ units: 0
#0 PULSE     cov: 0 corp: 0 exec/s: 0 rss: 29.72 MB
#1 NEW     cov: 1958 corp: 0 exec/s: 3 rss: 172.35 MB
#2 NEW     cov: 2001 corp: 1 exec/s: 142 rss: 172.37 MB
#8 NEW     cov: 2005 corp: 2 exec/s: 375 rss: 172.37 MB
#12 NEW     cov: 2030 corp: 3 exec/s: 1000 rss: 172.37 MB
#16 NEW     cov: 2053 corp: 4 exec/s: 1000 rss: 172.37 MB
#32 NEW     cov: 2066 corp: 5 exec/s: 888 rss: 172.43 MB
#99 NEW     cov: 2091 corp: 6 exec/s: 848 rss: 175.77 MB
#110 NEW     cov: 2207 corp: 7 exec/s: 647 rss: 176.34 MB
#156 NEW     cov: 2228 corp: 8 exec/s: 754 rss: 176.85 MB
#162 NEW     cov: 2256 corp: 9 exec/s: 857 rss: 177.39 MB
#194 NEW     cov: 2260 corp: 10 exec/s: 1000 rss: 177.39 MB
#206 NEW     cov: 2261 corp: 11 exec/s: 857 rss: 177.39 MB
#246 NEW     cov: 2269 corp: 12 exec/s: 1025 rss: 177.39 MB
#283 NEW     cov: 2270 corp: 13 exec/s: 973 rss: 177.39 MB
#330 NEW     cov: 2274 corp: 14 exec/s: 1000 rss: 177.39 MB
#416 NEW     cov: 2276 corp: 15 exec/s: 1088 rss: 177.51 MB
#446 NEW     cov: 2279 corp: 16 exec/s: 1000 rss: 177.51 MB
#506 NEW     cov: 2376 corp: 17 exec/s: 1000 rss: 177.53 MB
#557 NEW     cov: 2378 corp: 18 exec/s: 1000 rss: 177.55 MB
#817 NEW     cov: 2423 corp: 19 exec/s: 866 rss: 177.97 MB
#913 NEW     cov: 2426 corp: 20 exec/s: 932 rss: 178.77 MB
#915 NEW     cov: 2432 corp: 21 exec/s: 1000 rss: 178.77 MB
#1020 NEW     cov: 2435 corp: 22 exec/s: 981 rss: 178.78 MB
#1195 NEW     cov: 2450 corp: 23 exec/s: 862 rss: 178.97 MB
#1275 NEW     cov: 2452 corp: 24 exec/s: 792 rss: 179.58 MB
#1331 NEW     cov: 2454 corp: 25 exec/s: 1018 rss: 179.99 MB
#1375 NEW     cov: 2456 corp: 26 exec/s: 956 rss: 180.34 MB
#1434 NEW     cov: 2458 corp: 27 exec/s: 766 rss: 180.84 MB
#1494 NEW     cov: 2460 corp: 28 exec/s: 800 rss: 180.85 MB
#1590 NEW     cov: 2462 corp: 29 exec/s: 793 rss: 180.9 MB
#1706 NEW     cov: 2484 corp: 30 exec/s: 748 rss: 180.92 MB
#1871 NEW     cov: 2500 corp: 31 exec/s: 833 rss: 180.92 MB
#1903 NEW     cov: 2503 corp: 32 exec/s: 711 rss: 180.93 MB
#1919 NEW     cov: 2507 corp: 33 exec/s: 842 rss: 180.93 MB
#2149 NEW     cov: 2517 corp: 34 exec/s: 725 rss: 181.01 MB
#2181 NEW     cov: 2520 corp: 35 exec/s: 727 rss: 181.01 MB
#2252 PULSE     cov: 2520 corp: 36 exec/s: 710 rss: 181.01 MB
#2263 NEW     cov: 2522 corp: 36 exec/s: 523 rss: 191.79 MB
#2723 NEW     cov: 2523 corp: 37 exec/s: 731 rss: 191.83 MB
#2730 NEW     cov: 2524 corp: 38 exec/s: 583 rss: 191.83 MB
#2786 NEW     cov: 2543 corp: 39 exec/s: 811 rss: 191.84 MB
#3274 NEW     cov: 2547 corp: 40 exec/s: 770 rss: 192 MB
#3803 NEW     cov: 2555 corp: 41 exec/s: 776 rss: 192.09 MB
#4040 NEW     cov: 2557 corp: 42 exec/s: 690 rss: 192.3 MB
#4481 PULSE     cov: 2557 corp: 43 exec/s: 720 rss: 192.3 MB
#4660 NEW     cov: 2559 corp: 43 exec/s: 821 rss: 192.38 MB
#5817 NEW     cov: 2561 corp: 44 exec/s: 756 rss: 192.46 MB
#5871 NEW     cov: 2570 corp: 45 exec/s: 620 rss: 192.46 MB
#6547 NEW     cov: 2576 corp: 46 exec/s: 752 rss: 192.55 MB
#6713 PULSE     cov: 2576 corp: 47 exec/s: 619 rss: 192.55 MB
#8187 NEW     cov: 2582 corp: 47 exec/s: 810 rss: 195.6 MB
#8473 NEW     cov: 2585 corp: 48 exec/s: 711 rss: 195.6 MB
#9061 NEW     cov: 2587 corp: 49 exec/s: 824 rss: 195.6 MB
#9111 PULSE     cov: 2587 corp: 50 exec/s: 746 rss: 195.64 MB
#11501 PULSE     cov: 2587 corp: 50 exec/s: 796 rss: 197.14 MB
#13904 PULSE     cov: 2587 corp: 50 exec/s: 801 rss: 198.03 MB
#16326 PULSE     cov: 2587 corp: 50 exec/s: 807 rss: 198.68 MB
#18779 PULSE     cov: 2587 corp: 50 exec/s: 817 rss: 199.6 MB
#21223 PULSE     cov: 2587 corp: 50 exec/s: 814 rss: 203.31 MB
#23665 PULSE     cov: 2587 corp: 50 exec/s: 814 rss: 207.01 MB
#25519 NEW     cov: 2608 corp: 50 exec/s: 806 rss: 213.17 MB
#26059 PULSE     cov: 2608 corp: 51 exec/s: 769 rss: 213.17 MB
#28444 PULSE     cov: 2608 corp: 51 exec/s: 794 rss: 176.61 MB
#30897 PULSE     cov: 2608 corp: 51 exec/s: 817 rss: 146.25 MB
#32817 NEW     cov: 2610 corp: 51 exec/s: 804 rss: 146.25 MB
#33284 PULSE     cov: 2610 corp: 52 exec/s: 760 rss: 146.28 MB
#35632 NEW     cov: 2614 corp: 52 exec/s: 806 rss: 141.19 MB
#35689 PULSE     cov: 2614 corp: 53 exec/s: 640 rss: 141.19 MB
#36302 NEW     cov: 2616 corp: 53 exec/s: 799 rss: 141.02 MB
#38021 PULSE     cov: 2616 corp: 54 exec/s: 769 rss: 141.03 MB
#40329 PULSE     cov: 2616 corp: 54 exec/s: 769 rss: 140.35 MB
#42614 PULSE     cov: 2616 corp: 54 exec/s: 761 rss: 140.46 MB
#45047 PULSE     cov: 2616 corp: 54 exec/s: 811 rss: 140.56 MB
#47468 PULSE     cov: 2616 corp: 54 exec/s: 807 rss: 138.84 MB
#49899 PULSE     cov: 2616 corp: 54 exec/s: 810 rss: 137.63 MB
#52311 PULSE     cov: 2616 corp: 54 exec/s: 804 rss: 136.08 MB
#54689 PULSE     cov: 2616 corp: 54 exec/s: 792 rss: 135.11 MB
#56120 NEW     cov: 2618 corp: 54 exec/s: 840 rss: 131.97 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 131.97 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 132.05 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 132.05 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 132.05 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 132.05 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 127.02 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.76 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.77 MB
#56120 PULSE     cov: 2618 corp: 55 exec/s: 0 rss: 122.64 MB
...

It goes on like that forever. I tried killing it, and restarting, and it happens again, just in a different spot:

...
#88029 NEW     cov: 2614 corp: 57 exec/s: 719 rss: 141.23 MB
#88842 PULSE     cov: 2614 corp: 58 exec/s: 689 rss: 141.23 MB
#90975 PULSE     cov: 2614 corp: 58 exec/s: 711 rss: 140.23 MB
#93196 PULSE     cov: 2614 corp: 58 exec/s: 740 rss: 135.98 MB
#95483 PULSE     cov: 2614 corp: 58 exec/s: 762 rss: 136.55 MB
#97647 PULSE     cov: 2614 corp: 58 exec/s: 721 rss: 136.71 MB
#99854 PULSE     cov: 2614 corp: 58 exec/s: 735 rss: 136.55 MB
#101142 NEW     cov: 2619 corp: 58 exec/s: 724 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
#101142 PULSE     cov: 2619 corp: 59 exec/s: 0 rss: 137.1 MB
...

A third run seems to go on fine for as long as I'm willing to wait, so it's not guaranteed to happen.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.