├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── configs
├── chromium.json
├── command.json
├── firefox-aws.json
├── firefox.json
├── quokka.json
└── testsuites.txt
├── core
├── __init__.py
├── config.py
├── listeners
│ ├── __init__.py
│ ├── sanitizer.py
│ └── testcase.py
├── logger.py
├── loggers
│ ├── __init__.py
│ ├── filesystem.py
│ └── fuzzmanager.py
├── monitor.py
├── monitors
│ ├── __init__.py
│ ├── console.py
│ └── websocket.py
├── plugin.py
├── plugins
│ ├── __init__.py
│ ├── command.py
│ └── firefox.py
├── quokka.py
└── websocket.py
└── quokka.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 |
55 | # Sphinx documentation
56 | docs/_build/
57 |
58 | # PyBuilder
59 | target/
60 |
61 | # PyCharm
62 | .idea/
63 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Community Participation Guidelines
2 |
3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines.
4 | For more details, please read the
5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
6 |
7 | ## How to Report
8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
9 |
10 |
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License, version 2.0
2 |
3 | 1. Definitions
4 |
5 | 1.1. "Contributor"
6 |
7 | means each individual or legal entity that creates, contributes to the
8 | creation of, or owns Covered Software.
9 |
10 | 1.2. "Contributor Version"
11 |
12 | means the combination of the Contributions of others (if any) used by a
13 | Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 |
17 | means Covered Software of a particular Contributor.
18 |
19 | 1.4. "Covered Software"
20 |
21 | means Source Code Form to which the initial Contributor has attached the
22 | notice in Exhibit A, the Executable Form of such Source Code Form, and
23 | Modifications of such Source Code Form, in each case including portions
24 | thereof.
25 |
26 | 1.5. "Incompatible With Secondary Licenses"
27 | means
28 |
29 | a. that the initial Contributor has attached the notice described in
30 | Exhibit B to the Covered Software; or
31 |
32 | b. that the Covered Software was made available under the terms of
33 | version 1.1 or earlier of the License, but not also under the terms of
34 | a Secondary License.
35 |
36 | 1.6. "Executable Form"
37 |
38 | means any form of the work other than Source Code Form.
39 |
40 | 1.7. "Larger Work"
41 |
42 | means a work that combines Covered Software with other material, in a
43 | separate file or files, that is not Covered Software.
44 |
45 | 1.8. "License"
46 |
47 | means this document.
48 |
49 | 1.9. "Licensable"
50 |
51 | means having the right to grant, to the maximum extent possible, whether
52 | at the time of the initial grant or subsequently, any and all of the
53 | rights conveyed by this License.
54 |
55 | 1.10. "Modifications"
56 |
57 | means any of the following:
58 |
59 | a. any file in Source Code Form that results from an addition to,
60 | deletion from, or modification of the contents of Covered Software; or
61 |
62 | b. any new file in Source Code Form that contains any Covered Software.
63 |
64 | 1.11. "Patent Claims" of a Contributor
65 |
66 | means any patent claim(s), including without limitation, method,
67 | process, and apparatus claims, in any patent Licensable by such
68 | Contributor that would be infringed, but for the grant of the License,
69 | by the making, using, selling, offering for sale, having made, import,
70 | or transfer of either its Contributions or its Contributor Version.
71 |
72 | 1.12. "Secondary License"
73 |
74 | means either the GNU General Public License, Version 2.0, the GNU Lesser
75 | General Public License, Version 2.1, the GNU Affero General Public
76 | License, Version 3.0, or any later versions of those licenses.
77 |
78 | 1.13. "Source Code Form"
79 |
80 | means the form of the work preferred for making modifications.
81 |
82 | 1.14. "You" (or "Your")
83 |
84 | means an individual or a legal entity exercising rights under this
85 | License. For legal entities, "You" includes any entity that controls, is
86 | controlled by, or is under common control with You. For purposes of this
87 | definition, "control" means (a) the power, direct or indirect, to cause
88 | the direction or management of such entity, whether by contract or
89 | otherwise, or (b) ownership of more than fifty percent (50%) of the
90 | outstanding shares or beneficial ownership of such entity.
91 |
92 |
93 | 2. License Grants and Conditions
94 |
95 | 2.1. Grants
96 |
97 | Each Contributor hereby grants You a world-wide, royalty-free,
98 | non-exclusive license:
99 |
100 | a. under intellectual property rights (other than patent or trademark)
101 | Licensable by such Contributor to use, reproduce, make available,
102 | modify, display, perform, distribute, and otherwise exploit its
103 | Contributions, either on an unmodified basis, with Modifications, or
104 | as part of a Larger Work; and
105 |
106 | b. under Patent Claims of such Contributor to make, use, sell, offer for
107 | sale, have made, import, and otherwise transfer either its
108 | Contributions or its Contributor Version.
109 |
110 | 2.2. Effective Date
111 |
112 | The licenses granted in Section 2.1 with respect to any Contribution
113 | become effective for each Contribution on the date the Contributor first
114 | distributes such Contribution.
115 |
116 | 2.3. Limitations on Grant Scope
117 |
118 | The licenses granted in this Section 2 are the only rights granted under
119 | this License. No additional rights or licenses will be implied from the
120 | distribution or licensing of Covered Software under this License.
121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
122 | Contributor:
123 |
124 | a. for any code that a Contributor has removed from Covered Software; or
125 |
126 | b. for infringements caused by: (i) Your and any other third party's
127 | modifications of Covered Software, or (ii) the combination of its
128 | Contributions with other software (except as part of its Contributor
129 | Version); or
130 |
131 | c. under Patent Claims infringed by Covered Software in the absence of
132 | its Contributions.
133 |
134 | This License does not grant any rights in the trademarks, service marks,
135 | or logos of any Contributor (except as may be necessary to comply with
136 | the notice requirements in Section 3.4).
137 |
138 | 2.4. Subsequent Licenses
139 |
140 | No Contributor makes additional grants as a result of Your choice to
141 | distribute the Covered Software under a subsequent version of this
142 | License (see Section 10.2) or under the terms of a Secondary License (if
143 | permitted under the terms of Section 3.3).
144 |
145 | 2.5. Representation
146 |
147 | Each Contributor represents that the Contributor believes its
148 | Contributions are its original creation(s) or it has sufficient rights to
149 | grant the rights to its Contributions conveyed by this License.
150 |
151 | 2.6. Fair Use
152 |
153 | This License is not intended to limit any rights You have under
154 | applicable copyright doctrines of fair use, fair dealing, or other
155 | equivalents.
156 |
157 | 2.7. Conditions
158 |
159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
160 | Section 2.1.
161 |
162 |
163 | 3. Responsibilities
164 |
165 | 3.1. Distribution of Source Form
166 |
167 | All distribution of Covered Software in Source Code Form, including any
168 | Modifications that You create or to which You contribute, must be under
169 | the terms of this License. You must inform recipients that the Source
170 | Code Form of the Covered Software is governed by the terms of this
171 | License, and how they can obtain a copy of this License. You may not
172 | attempt to alter or restrict the recipients' rights in the Source Code
173 | Form.
174 |
175 | 3.2. Distribution of Executable Form
176 |
177 | If You distribute Covered Software in Executable Form then:
178 |
179 | a. such Covered Software must also be made available in Source Code Form,
180 | as described in Section 3.1, and You must inform recipients of the
181 | Executable Form how they can obtain a copy of such Source Code Form by
182 | reasonable means in a timely manner, at a charge no more than the cost
183 | of distribution to the recipient; and
184 |
185 | b. You may distribute such Executable Form under the terms of this
186 | License, or sublicense it under different terms, provided that the
187 | license for the Executable Form does not attempt to limit or alter the
188 | recipients' rights in the Source Code Form under this License.
189 |
190 | 3.3. Distribution of a Larger Work
191 |
192 | You may create and distribute a Larger Work under terms of Your choice,
193 | provided that You also comply with the requirements of this License for
194 | the Covered Software. If the Larger Work is a combination of Covered
195 | Software with a work governed by one or more Secondary Licenses, and the
196 | Covered Software is not Incompatible With Secondary Licenses, this
197 | License permits You to additionally distribute such Covered Software
198 | under the terms of such Secondary License(s), so that the recipient of
199 | the Larger Work may, at their option, further distribute the Covered
200 | Software under the terms of either this License or such Secondary
201 | License(s).
202 |
203 | 3.4. Notices
204 |
205 | You may not remove or alter the substance of any license notices
206 | (including copyright notices, patent notices, disclaimers of warranty, or
207 | limitations of liability) contained within the Source Code Form of the
208 | Covered Software, except that You may alter any license notices to the
209 | extent required to remedy known factual inaccuracies.
210 |
211 | 3.5. Application of Additional Terms
212 |
213 | You may choose to offer, and to charge a fee for, warranty, support,
214 | indemnity or liability obligations to one or more recipients of Covered
215 | Software. However, You may do so only on Your own behalf, and not on
216 | behalf of any Contributor. You must make it absolutely clear that any
217 | such warranty, support, indemnity, or liability obligation is offered by
218 | You alone, and You hereby agree to indemnify every Contributor for any
219 | liability incurred by such Contributor as a result of warranty, support,
220 | indemnity or liability terms You offer. You may include additional
221 | disclaimers of warranty and limitations of liability specific to any
222 | jurisdiction.
223 |
224 | 4. Inability to Comply Due to Statute or Regulation
225 |
226 | If it is impossible for You to comply with any of the terms of this License
227 | with respect to some or all of the Covered Software due to statute,
228 | judicial order, or regulation then You must: (a) comply with the terms of
229 | this License to the maximum extent possible; and (b) describe the
230 | limitations and the code they affect. Such description must be placed in a
231 | text file included with all distributions of the Covered Software under
232 | this License. Except to the extent prohibited by statute or regulation,
233 | such description must be sufficiently detailed for a recipient of ordinary
234 | skill to be able to understand it.
235 |
236 | 5. Termination
237 |
238 | 5.1. The rights granted under this License will terminate automatically if You
239 | fail to comply with any of its terms. However, if You become compliant,
240 | then the rights granted under this License from a particular Contributor
241 | are reinstated (a) provisionally, unless and until such Contributor
242 | explicitly and finally terminates Your grants, and (b) on an ongoing
243 | basis, if such Contributor fails to notify You of the non-compliance by
244 | some reasonable means prior to 60 days after You have come back into
245 | compliance. Moreover, Your grants from a particular Contributor are
246 | reinstated on an ongoing basis if such Contributor notifies You of the
247 | non-compliance by some reasonable means, this is the first time You have
248 | received notice of non-compliance with this License from such
249 | Contributor, and You become compliant prior to 30 days after Your receipt
250 | of the notice.
251 |
252 | 5.2. If You initiate litigation against any entity by asserting a patent
253 | infringement claim (excluding declaratory judgment actions,
254 | counter-claims, and cross-claims) alleging that a Contributor Version
255 | directly or indirectly infringes any patent, then the rights granted to
256 | You by any and all Contributors for the Covered Software under Section
257 | 2.1 of this License shall terminate.
258 |
259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
260 | license agreements (excluding distributors and resellers) which have been
261 | validly granted by You or Your distributors under this License prior to
262 | termination shall survive termination.
263 |
264 | 6. Disclaimer of Warranty
265 |
266 | Covered Software is provided under this License on an "as is" basis,
267 | without warranty of any kind, either expressed, implied, or statutory,
268 | including, without limitation, warranties that the Covered Software is free
269 | of defects, merchantable, fit for a particular purpose or non-infringing.
270 | The entire risk as to the quality and performance of the Covered Software
271 | is with You. Should any Covered Software prove defective in any respect,
272 | You (not any Contributor) assume the cost of any necessary servicing,
273 | repair, or correction. This disclaimer of warranty constitutes an essential
274 | part of this License. No use of any Covered Software is authorized under
275 | this License except under this disclaimer.
276 |
277 | 7. Limitation of Liability
278 |
279 | Under no circumstances and under no legal theory, whether tort (including
280 | negligence), contract, or otherwise, shall any Contributor, or anyone who
281 | distributes Covered Software as permitted above, be liable to You for any
282 | direct, indirect, special, incidental, or consequential damages of any
283 | character including, without limitation, damages for lost profits, loss of
284 | goodwill, work stoppage, computer failure or malfunction, or any and all
285 | other commercial damages or losses, even if such party shall have been
286 | informed of the possibility of such damages. This limitation of liability
287 | shall not apply to liability for death or personal injury resulting from
288 | such party's negligence to the extent applicable law prohibits such
289 | limitation. Some jurisdictions do not allow the exclusion or limitation of
290 | incidental or consequential damages, so this exclusion and limitation may
291 | not apply to You.
292 |
293 | 8. Litigation
294 |
295 | Any litigation relating to this License may be brought only in the courts
296 | of a jurisdiction where the defendant maintains its principal place of
297 | business and such litigation shall be governed by laws of that
298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing
299 | in this Section shall prevent a party's ability to bring cross-claims or
300 | counter-claims.
301 |
302 | 9. Miscellaneous
303 |
304 | This License represents the complete agreement concerning the subject
305 | matter hereof. If any provision of this License is held to be
306 | unenforceable, such provision shall be reformed only to the extent
307 | necessary to make it enforceable. Any law or regulation which provides that
308 | the language of a contract shall be construed against the drafter shall not
309 | be used to construe this License against a Contributor.
310 |
311 |
312 | 10. Versions of the License
313 |
314 | 10.1. New Versions
315 |
316 | Mozilla Foundation is the license steward. Except as provided in Section
317 | 10.3, no one other than the license steward has the right to modify or
318 | publish new versions of this License. Each version will be given a
319 | distinguishing version number.
320 |
321 | 10.2. Effect of New Versions
322 |
323 | You may distribute the Covered Software under the terms of the version
324 | of the License under which You originally received the Covered Software,
325 | or under the terms of any subsequent version published by the license
326 | steward.
327 |
328 | 10.3. Modified Versions
329 |
330 | If you create software not governed by this License, and you want to
331 | create a new license for such software, you may create and use a
332 | modified version of this License if you rename the license and remove
333 | any references to the name of the license steward (except to note that
334 | such modified license differs from this License).
335 |
336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
337 | Licenses If You choose to distribute Source Code Form that is
338 | Incompatible With Secondary Licenses under the terms of this version of
339 | the License, the notice described in Exhibit B of this License must be
340 | attached.
341 |
342 | Exhibit A - Source Code Form License Notice
343 |
344 | This Source Code Form is subject to the
345 | terms of the Mozilla Public License, v.
346 | 2.0. If a copy of the MPL was not
347 | distributed with this file, You can
348 | obtain one at
349 | http://mozilla.org/MPL/2.0/.
350 |
351 | If it is not possible or desirable to put the notice in a particular file,
352 | then You may include the notice in a location (such as a LICENSE file in a
353 | relevant directory) where a recipient would be likely to look for such a
354 | notice.
355 |
356 | You may add additional accurate notices of copyright ownership.
357 |
358 | Exhibit B - "Incompatible With Secondary Licenses" Notice
359 |
360 | This Source Code Form is "Incompatible
361 | With Secondary Licenses", as defined by
362 | the Mozilla Public License, v. 2.0.
363 |
364 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 | Quokka is a utility to launch and monitor application for faults. This includes testcase verification, monitoring of unittests and testsuites or using it as harness for fuzzers like Dharma.
5 |
6 |
Requirements
7 | ---
8 | None
9 |
10 |
11 | Basic Usage Examples
12 | ---
13 | Launch an application with a plugin class and plugin configuration.
14 | ```
15 | ./quokka.py -plugin configs/firefox.json
16 | ```
17 |
18 | It is possible to declare **@macros@** in the configuration files and define those over the command-line.
19 |
20 | ```
21 | ./quokka.py -plugin configs/firefox.json -conf-vars params=/srv/fuzzers/dharma/grammars/var/index.html
22 | ```
23 |
24 | To find out wheather or which macros are declared in a configuration file you can run the following command.
25 | ```
26 | ./quokka.py -list-conf-vars -plugin configs/firefox.json
27 | [Quokka] 2015-05-14 18:49:44 INFO: List of available configuration variables:
28 | [Quokka] 2015-05-14 18:49:44 INFO: 'params'
29 | ```
30 |
31 | You can use a dot-notation of keys to access and edit nested configuration values.
32 | ```
33 | ./quokka.py -plugin configs/firefox.json -conf-vars params=google.com -conf-args environ.ASAN_OPTIONS.strict_init_order=1
34 | ```
35 |
36 | Quokka comes with a command.json configuration, in order execute simple programs which do not need complex setup routines.
37 |
38 | ```
39 | ./quokka.py -plugin configs/command.json -conf-args plugin.kargs.binary=/sbin/ping plugin.kargs.params="-c 10 google.com"
40 | ```
41 |
42 | Configurations
43 | ---
44 |
45 | There are two types of configurations. The main Quokka configuration and a plugin configuration. The Quokka configuration is the main configuration which is used to define default values.
46 |
47 |
48 | **Example: quokka.conf**
49 |
50 | ```
51 | {
52 | "environ": {
53 | "ASAN_OPTIONS": {},
54 | "ASAN_SYMBOLIZE": "/srv/repos/llvm/r233758/build/bin/llvm-symbolizer"
55 | },
56 | "loggers": [
57 | {
58 | "class": "filesystem.FileLogger",
59 | "kargs": {
60 | "path": "/srv/logs"
61 | }
62 | }
63 | ],
64 | "monitors": [
65 | {
66 | "class": "console.ConsoleMonitor",
67 | "kargs": {},
68 | "listeners": [
69 | {
70 | "class": "sanitizer.ASanListener",
71 | "kargs": {}
72 | },
73 | {
74 | "class": "testcase.TestcaseListener",
75 | "kargs": {}
76 | }
77 | ]
78 | }
79 | ]
80 | }
81 | ```
82 |
83 | **Example: plugin.conf**
84 |
85 | ```
86 | {
87 | "plugin": {
88 | "class": "command.ConsoleApplication",
89 | "kargs": {
90 | "binary": "",
91 | "params": ""
92 | }
93 | }
94 | }
95 | ```
96 |
97 |
98 |
99 | Help Menu
100 | ---
101 | ```
102 | usage: ./quokka.py -plugin file [-quokka file] [-conf-args k=v [k=v ...]]
103 | [-conf-vars k=v [k=v ...]] [-list-conf-vars]
104 | [-verbosity {1..5}]
105 |
106 | Quokka Runtime
107 |
108 | Mandatory Arguments:
109 | -plugin file Run an application. (default: None)
110 |
111 | Optional Arguments:
112 | -quokka file Quokka configuration (default: configs/quokka.json)
113 | -conf-args k=v [k=v ...]
114 | Add/edit configuration properties. (default: None)
115 | -conf-vars k=v [k=v ...]
116 | Subsitute configuration variables. (default: None)
117 | -list-conf-vars List used configuration variables. (default: False)
118 | -verbosity {1..5} Level of verbosity for logging module. (default: 2)
119 |
120 | The exit status is 0 for non-failures and 1 for failures.
121 | ```
122 |
--------------------------------------------------------------------------------
/configs/chromium.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "class": "command.ConsoleApplication",
4 | "kargs": {
5 | "binary": "/Users/cdiehl/Documents/repos/chromium/asan-mac-release-328075/Chromium.app/Contents/MacOS/Chromium",
6 | "params": "--enable-logging=stderr --log-level=0 --js-flags='--expose_gc' --allow-file-access-from-files --user-data-dir=/tmp/chromium --window-size=512,512 --disable-popup-blocking --ignore-certificate-errors --enable-experimental-canvas-features --enable-web-midi --enable-spdy4a2 --enable-opus-playback --enable-vp8-alpha-playback --enable-experimental-web-platform-features --enable-deferred-image-decoding --enable-usermedia-screen-capture --enable-webgl-draft-extensions --enable-video-track --single-process --no-sandbox --disable-seccomp-filter-sandbox --disable-seccomp-sandbox --crash-on-hang-seconds=60 --crash-on-hang-threads=60 @params@"
7 | }
8 | },
9 | "environ": {
10 | "DYLD_LIBRARY_PATH": "/Users/cdiehl/Documents/repos/chromium/asan-mac-release-328075/Chromium.app/Contents/MacOS"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/configs/command.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "class": "command.ConsoleApplication",
4 | "kargs": {
5 | "binary": "",
6 | "params": ""
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/configs/firefox-aws.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "class": "firefox.FirefoxApplication",
4 | "kargs": {
5 | "binary": "/home/ubuntu/firefox/firefox",
6 | "params": "-no-remote -height 512 -width 512 @params@",
7 | "prefs": "/home/ubuntu/Resources/Settings/Firefox/prefs.js"
8 | }
9 | },
10 | "environ": {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/configs/firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "class": "firefox.FirefoxApplication",
4 | "kargs": {
5 | "binary": "/Users/cdiehl/Documents/repos/mozilla/mozilla-inbound/obj-ff64-asan-opt/dist/NightlyDebug.app/Contents/MacOS/firefox",
6 | "params": "-no-remote -height 512 -width 512 @params@",
7 | "prefs": "/Users/cdiehl/Dropbox/Projects/peach/peach/Resources/Settings/Firefox/prefs.js"
8 | }
9 | },
10 | "environ": {
11 | "DYLD_LIBRARY_PATH": "/Users/cdiehl/Documents/repos/mozilla/mozilla-inbound/obj-ff64-asan-opt/dist/NightlyDebug.app/Contents/MacOS/"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/configs/quokka.json:
--------------------------------------------------------------------------------
1 | {
2 | "environ": {
3 | "ASAN_OPTIONS": {
4 | "strict_init_order": 0,
5 | "strict_memcmp": 0,
6 | "allow_user_poisoning": 0,
7 | "check_malloc_usable_size": 0,
8 | "alloc_dealloc_mismatch": 0
9 | },
10 | "ASAN_SYMBOLIZE": "/srv/repos/llvm/r233758/build/bin/llvm-symbolizer"
11 | },
12 | "loggers": [
13 | {
14 | "class": "filesystem.FileLogger",
15 | "kargs": {
16 | "path": "/srv/logs"
17 | }
18 | },
19 | {
20 | "class": "fuzzmanager.FuzzManager",
21 | "kargs": {
22 | "binary": ""
23 | }
24 | }
25 | ],
26 | "monitors": [
27 | {
28 | "class": "console.ConsoleMonitor",
29 | "kargs": {},
30 | "listeners": [
31 | {
32 | "class": "sanitizer.ASanListener",
33 | "kargs": {}
34 | },
35 | {
36 | "class": "testcase.TestcaseListener",
37 | "kargs": {}
38 | }
39 | ]
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/configs/testsuites.txt:
--------------------------------------------------------------------------------
1 | https://www.khronos.org/registry/webgl/sdk/tests/webgl-conformance-tests.html?version=1.0.4&run=1
2 | https://www.khronos.org/registry/webgl/sdk/tests/webgl-conformance-tests.html?version=2.0.0&run=1
3 | https://github.com/w3c/web-platform-tests
4 |
--------------------------------------------------------------------------------
/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MozillaSecurity/quokka/8b9c64b51cd34f4ea8842432df1ecb99d2aba7dc/core/__init__.py
--------------------------------------------------------------------------------
/core/config.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import re
6 | import json
7 | import logging
8 |
9 | from .quokka import QuokkaException
10 |
11 |
12 | class AttributeTree(dict):
13 |
14 | def __init__(self, value=None):
15 | if value is None:
16 | pass
17 | elif isinstance(value, dict):
18 | for key in value:
19 | self.__setitem__(key, value[key])
20 | else:
21 | raise TypeError('Expected dict()')
22 |
23 | def __setitem__(self, key, value):
24 | if '.' in key:
25 | my_key, rest_of_key = key.split('.', 1)
26 | target = self.setdefault(my_key, AttributeTree())
27 | if not isinstance(target, AttributeTree):
28 | raise KeyError('Can not set "%s" in "%s" (%s)' % (rest_of_key, my_key, repr(target)))
29 | target[rest_of_key] = value
30 | else:
31 | if isinstance(value, dict) and not isinstance(value, AttributeTree):
32 | value = AttributeTree(value)
33 | dict.__setitem__(self, key, value)
34 |
35 | def __getitem__(self, key):
36 | if '.' not in key:
37 | return dict.__getitem__(self, key)
38 | my_key, rest_of_key = key.split('.', 1)
39 | target = dict.__getitem__(self, my_key)
40 | if not isinstance(target, AttributeTree):
41 | raise KeyError('Can not get "%s" in "%s" (%s)' % (rest_of_key, my_key, repr(target)))
42 | return target[rest_of_key]
43 |
44 | def __contains__(self, key):
45 | if '.' not in key:
46 | return dict.__contains__(self, key)
47 | my_key, rest_of_key = key.split('.', 1)
48 | target = dict.__getitem__(self, my_key)
49 | if not isinstance(target, AttributeTree):
50 | return False
51 | return rest_of_key in target
52 |
53 | def setdefault(self, key, default):
54 | if key not in self:
55 | self[key] = default
56 | return self[key]
57 |
58 | __setattr__ = __setitem__
59 | __getattr__ = __getitem__
60 |
61 |
62 | class QuokkaConf(object):
63 |
64 | def __init__(self, conf):
65 | try:
66 | conf = json.loads(conf)
67 | except ValueError as msg:
68 | raise QuokkaException('Unable to parse Quokka configuration: %s' % msg)
69 | self.quokka = AttributeTree(conf)
70 | self.plugin = {}
71 |
72 | def add_plugin_conf(self, conf):
73 | try:
74 | conf = json.loads(conf)
75 | except ValueError as msg:
76 | raise QuokkaException('Unable to parse plugin configuration: %s' % msg)
77 | self.plugin = AttributeTree(conf)
78 | logging.info('Merging plugin configuration with Quokka.')
79 | self.quokka = AttributeTree(self.merge(self.plugin, self.quokka))
80 |
81 | @staticmethod
82 | def merge(x, y):
83 | merged = dict(x, **y) # a copy of |x| but overwrite with |y|'s values where applicable.
84 | xkeys = x.keys()
85 | # If the value of merged[key] was overwritten with y[key]'s value, we put back any missing x[key] values.
86 | for key in xkeys:
87 | if isinstance(x[key], dict) and key in y:
88 | merged[key] = QuokkaConf.merge(x[key], y[key])
89 | return merged
90 |
91 | @staticmethod
92 | def set_conf_vars(conf, vars):
93 | conf_vars = re.findall("@(.*?)@", conf)
94 | for var in conf_vars:
95 | if var not in vars:
96 | logging.error('Undefined variable @%s@ in configuration', var)
97 | return
98 | conf = conf.replace('@%s@' % var, vars[var])
99 | return conf
100 |
101 | @staticmethod
102 | def list_conf_vars(conf):
103 | return re.findall("@(.*?)@", conf)
104 |
105 | @property
106 | def monitors(self):
107 | monitors = self.quokka.get("monitors")
108 | if not monitors:
109 | raise QuokkaException("No monitors to attach.")
110 | return monitors
111 |
112 | @property
113 | def loggers(self):
114 | loggers = self.quokka.get("loggers")
115 | if not loggers:
116 | raise QuokkaException("No loggers to attach.")
117 | return loggers
118 |
119 | @property
120 | def plugin_root(self):
121 | plugin_root = self.quokka.get("plugin")
122 | if not plugin_root:
123 | raise QuokkaException("Malformed plugin structure.")
124 | return plugin_root
125 |
126 | @property
127 | def plugin_class(self):
128 | plugin_class = self.plugin_root.get("class")
129 | if not plugin_class:
130 | raise QuokkaException("Plugin class is not defined.")
131 | return plugin_class
132 |
133 | @property
134 | def plugin_kargs(self):
135 | plugin_kargs = self.plugin_root.get("kargs")
136 | if not plugin_kargs:
137 | raise QuokkaException("Plugin kargs is not defined.")
138 | return plugin_kargs
139 |
--------------------------------------------------------------------------------
/core/listeners/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MozillaSecurity/quokka/8b9c64b51cd34f4ea8842432df1ecb99d2aba7dc/core/listeners/__init__.py
--------------------------------------------------------------------------------
/core/listeners/sanitizer.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 |
7 | from ..monitor import Listener
8 |
9 |
10 | class ASanListener(Listener):
11 | LISTENER_NAME = 'AsanListener'
12 |
13 | def __init__(self, *args):
14 | super(ASanListener, self).__init__(*args)
15 | self.crashlog = []
16 | self.failure = False
17 |
18 | def process_line(self, line):
19 | if line.find('ERROR: AddressSanitizer') != -1:
20 | self.failure = True
21 | if self.failure:
22 | self.crashlog.append(line)
23 |
24 | def detected_fault(self):
25 | return self.failure
26 |
27 | def get_data(self, bucket):
28 | if self.crashlog:
29 | bucket['crashlog'] = {
30 | 'data': os.linesep.join(self.crashlog),
31 | 'name': 'crashlog.txt'
32 | }
33 |
34 |
35 | class SyzyListener(Listener):
36 |
37 | LISTENER = 'SyzyAsanListener'
38 |
39 | def __init__(self, *args):
40 | super(SyzyListener, self).__init__(*args)
41 | self.crashlog = []
42 | self.failure = False
43 |
44 | def process_line(self, line):
45 | if line.find('SyzyASAN error:') != -1:
46 | self.failure = True
47 | if self.failure:
48 | self.crashlog.append(line)
49 |
50 | def detected_fault(self):
51 | return self.failure
52 |
53 | def get_data(self, bucket):
54 | if self.crashlog:
55 | bucket['crashlog'] = {
56 | 'data': os.linesep.join(self.crashlog),
57 | 'name': 'crashlog.txt'
58 | }
59 |
--------------------------------------------------------------------------------
/core/listeners/testcase.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 | import json
7 |
8 | from ..monitor import Listener
9 |
10 |
11 | class TestcaseListener(Listener):
12 | LISTENER_NAME = 'TestcaseListener'
13 |
14 | def __init__(self, *args):
15 | super(TestcaseListener, self).__init__(*args)
16 | self.testcase = []
17 |
18 | def process_line(self, line):
19 | if line.find('NEXT TESTCASE') != -1:
20 | self.testcase = []
21 | #if line.find("/*L*/") != -1: # For Chromium
22 | if line.startswith('/*L*/'):
23 | self.testcase.append(json.loads(line[5:]))
24 |
25 | def detected_fault(self):
26 | return True
27 |
28 | def get_data(self, bucket):
29 | if self.testcase:
30 | bucket['testcase'] = {
31 | 'data': os.linesep.join(self.testcase),
32 | 'name': 'testcase.txt'
33 | }
34 |
--------------------------------------------------------------------------------
/core/logger.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 |
6 |
7 | class Logger(object):
8 | """
9 | Parent class for collecting buckets.
10 | """
11 |
12 | def __init__(self):
13 | self.bucket = {}
14 |
15 | def add_to_bucket(self, data):
16 | self.bucket.update(data)
17 |
18 | def add_fault(self):
19 | pass
20 |
--------------------------------------------------------------------------------
/core/loggers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MozillaSecurity/quokka/8b9c64b51cd34f4ea8842432df1ecb99d2aba7dc/core/loggers/__init__.py
--------------------------------------------------------------------------------
/core/loggers/filesystem.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 | import time
7 | import logging
8 |
9 | from ..logger import Logger
10 |
11 |
12 | class FileLogger(Logger):
13 | """
14 | Bucket class to save crash information to disk.
15 | """
16 |
17 | BUCKET_ID = 'quokka_{}'.format(time.strftime('%a_%b_%d_%H-%M-%S_%Y'))
18 |
19 | def __init__(self, **kwargs):
20 | super(FileLogger, self).__init__()
21 | self.__dict__.update(kwargs)
22 |
23 | self.bucketpath = os.path.join(self.path, self.BUCKET_ID)
24 | self.faultspath = os.path.join(self.bucketpath, 'faults')
25 | if not self.bucketpath:
26 | try:
27 | os.makedirs(self.bucketpath)
28 | except OSError as e:
29 | logging.exception(e)
30 |
31 | def add_fault(self):
32 | faultpath = os.path.join(self.faultspath, str(self.faults))
33 | try:
34 | os.makedirs(faultpath)
35 | except OSError as e:
36 | logging.exception(e)
37 | return
38 | for name, meta in self.bucket.items():
39 | if 'data' not in meta or not meta['data']:
40 | logging.error('Bucket "{}" does not contain "data" field or field is empty.'.format(name))
41 | continue
42 | if 'name' not in meta or not meta['name']:
43 | logging.error('Bucket "{}" does not contain "name" field or field is empty.'.format(name))
44 | continue
45 | filename = os.path.join(faultpath, meta['name'])
46 | try:
47 | with open(filename, 'wb') as fo:
48 | fo.write(meta['data'].encode('UTF-8'))
49 | except IOError as e:
50 | logging.exception(e)
51 |
52 | @property
53 | def faults(self):
54 | count = 0
55 | if not os.path.exists(self.faultspath):
56 | return count
57 | for item in os.listdir(self.faultspath):
58 | item = os.path.join(self.faultspath, item)
59 | if os.path.isdir(item) and not item.startswith('.'):
60 | count += 1
61 | return count
62 |
--------------------------------------------------------------------------------
/core/loggers/fuzzmanager.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import zipfile
6 | import logging
7 | import tempfile
8 | try:
9 | from io import StringIO
10 | except ImportError as msg:
11 | from StringIO import StringIO
12 | try:
13 | import sys
14 | sys.path.append("utils/FuzzManager")
15 | from FTB.ProgramConfiguration import ProgramConfiguration
16 | from FTB.Signatures.CrashInfo import CrashInfo
17 | from Collector.Collector import Collector
18 | except ImportError as msg:
19 | logging.warning("FuzzManager is missing or one of its dependencies: %s" % msg)
20 |
21 | from ..logger import Logger
22 |
23 |
24 | try:
25 | class FuzzManagerLogger(Logger):
26 | def __init__(self, **kwargs):
27 | super(FuzzManagerLogger, self).__init__()
28 |
29 | self.binary = None
30 |
31 | self.__dict__.update(kwargs)
32 |
33 | if not self.binary:
34 | raise Exception("Required FuzzManagerLogger setting 'binary' is missing.")
35 |
36 | def save_bucket_as_zip(self, bucket):
37 | """ Saves captured content of listeners as files to a zip archive.
38 |
39 | :param bucket: A dict in format of: {id: {name:'', data:''}}
40 | :return: The name of the zip archive.
41 | """
42 | buffer = StringIO()
43 | zip_buffer = zipfile.ZipFile(buffer, 'w')
44 | for name, meta in bucket.items():
45 | zip_buffer.writestr(meta["name"], meta["data"])
46 | zip_buffer.close()
47 | with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as testcase:
48 | buffer.seek(0)
49 | testcase.write(buffer.getvalue())
50 | testcase.close()
51 | return testcase.name
52 |
53 | def add_fault(self):
54 | # Setup FuzzManager with target information and platform data.
55 | program_configuration = ProgramConfiguration.fromBinary(self.binary)
56 |
57 | # Prepare FuzzManager with crash information.
58 | stdout = "N/A" # Todo: There is no plain stdout logger yet.
59 | stderr = "N/A" # Todo: There is no plain stderr logger yet.
60 | auxdat = self.bucket.get("crashlog", "N/A").get("data", "N/A")
61 | metaData = None
62 | testcase = self.save_bucket_as_zip(self.bucket)
63 | crash_info = CrashInfo.fromRawCrashData(stdout, stderr, program_configuration, auxdat)
64 |
65 | # Submit crash report with testcase to FuzzManager.
66 | collector = Collector(tool="dharma")
67 | collector.submit(crash_info, testcase, metaData)
68 | except Exception as msg:
69 | logging.error("FuzzManager is not available!")
70 |
--------------------------------------------------------------------------------
/core/monitor.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import time
6 | import threading
7 | try:
8 | from queue import Queue, Empty
9 | except ImportError as e:
10 | # Python 2
11 | from Queue import Queue, Empty
12 |
13 |
14 | class MonitorException(Exception):
15 | """
16 | Unrecoverable error in attached monitor.
17 | """
18 | pass
19 |
20 |
21 | class ListenerException(Exception):
22 | """
23 | Unrecoverable error in attached listener.
24 | """
25 | pass
26 |
27 |
28 | class Listener(object):
29 | """
30 | An abstract class for providing base methods and properties to listeners.
31 | """
32 |
33 | @classmethod
34 | def name(cls):
35 | return getattr(cls, 'LISTENER_NAME', cls.__name__)
36 |
37 | def process_line(self, line):
38 | pass
39 |
40 | def detected_fault(self):
41 | return False
42 |
43 | def get_data(self, bucket):
44 | pass
45 |
46 |
47 | class Monitor(threading.Thread):
48 | """
49 | An abstract class for providing base methods and properties to monitors.
50 | """
51 |
52 | def __init__(self, verbose=True):
53 | super(Monitor, self).__init__()
54 | self.verbose = verbose
55 | self.listeners = []
56 | self.line_queue = Queue()
57 |
58 | @classmethod
59 | def name(cls):
60 | return getattr(cls, 'MONITOR_NAME', cls.__name__)
61 |
62 | def run(self):
63 | line_consumer = threading.Thread(target=self.enqueue_lines)
64 | line_consumer.daemon = True
65 | line_consumer.start()
66 |
67 | while True:
68 | try:
69 | line = self.line_queue.get_nowait()
70 | except Empty:
71 | time.sleep(0.01)
72 | continue
73 |
74 | line = line.strip()
75 |
76 | if self.verbose:
77 | print(line)
78 |
79 | for listener in self.listeners:
80 | listener.process_line(line)
81 |
82 | def enqueue_lines(self):
83 | pass
84 |
85 | def stop(self):
86 | pass
87 |
88 | def add_listener(self, listener):
89 | assert isinstance(listener, Listener)
90 | self.listeners.append(listener)
91 |
92 | def detected_fault(self):
93 | return any(listener.detected_fault() for listener in self.listeners)
94 |
95 | def get_data(self):
96 | bucket = {}
97 | for listener in self.listeners:
98 | listener.get_data(bucket)
99 | return bucket
100 |
--------------------------------------------------------------------------------
/core/monitors/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MozillaSecurity/quokka/8b9c64b51cd34f4ea8842432df1ecb99d2aba7dc/core/monitors/__init__.py
--------------------------------------------------------------------------------
/core/monitors/console.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | from ..monitor import Monitor
6 |
7 |
8 | class ConsoleMonitor(Monitor):
9 | MONITOR_NAME = 'ConsoleMonitor'
10 |
11 | def __init__(self, process, *args, **kwargs):
12 | super(ConsoleMonitor, self).__init__(*args, **kwargs)
13 | self.out = process.stdout
14 |
15 | def enqueue_lines(self):
16 | for line in iter(self.out.readline, ''):
17 | self.line_queue.put(line)
18 | self.out.close()
19 |
--------------------------------------------------------------------------------
/core/monitors/websocket.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import logging
6 | try:
7 | from socketserver import TCPServer
8 | except ImportError:
9 | from SocketServer import TCPServer
10 |
11 | from .. import websocket
12 | from ..monitor import Monitor
13 |
14 |
15 | class WebSocketMonitor(Monitor):
16 | MONITOR_NAME = 'WebSocketMonitor'
17 |
18 | def __init__(self, addr_port=('', 9999), *args, **kwargs):
19 | super(WebSocketMonitor, self).__init__(*args, **kwargs)
20 | self.addr_port = addr_port
21 | self.server = None
22 |
23 | def enqueue_lines(self):
24 | run = True
25 | line_queue = self.line_queue
26 |
27 | class WebSocketHandler(websocket.BaseWebSocketHandler):
28 | def on_message(self, message):
29 | line_queue.put(message)
30 |
31 | def should_close(self):
32 | return not run
33 |
34 | class _TCPServer(TCPServer):
35 | allow_reuse_address = True
36 |
37 | self.server = _TCPServer(self.addr_port, WebSocketHandler)
38 | try:
39 | self.server.serve_forever()
40 | finally:
41 | run = False
42 |
43 | def stop(self):
44 | if self.server:
45 | try:
46 | self.server.shutdown()
47 | except Exception as e:
48 | logging.exception(e)
49 |
--------------------------------------------------------------------------------
/core/plugin.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 | import sys
7 | import time
8 | import logging
9 | import subprocess
10 |
11 |
12 | class PluginException(Exception):
13 | """
14 | Unrecoverable error in external process.
15 | """
16 | pass
17 |
18 |
19 | class BasePlugin(object):
20 | """
21 | An abstract class for providing base methods and properties to plugins.
22 | """
23 |
24 | def __init__(self, quokka):
25 | self.quokka = quokka
26 |
27 | @classmethod
28 | def name(cls):
29 | return getattr(cls, 'PLUGIN_NAME', cls.__name__)
30 |
31 | @classmethod
32 | def version(cls):
33 | return getattr(cls, 'PLUGIN_VERSION', '0.1')
34 |
35 | def start(self):
36 | pass
37 |
38 | def stop(self):
39 | pass
40 |
41 |
42 |
43 | class PluginProcess(BasePlugin):
44 | """
45 | Parent class for plugins which make use of external tools.
46 | """
47 |
48 | def __init__(self):
49 | self.process = None
50 |
51 | def open(self, cmd, env=None, cwd=None):
52 | logging.info('Running command: {}'.format(cmd))
53 | self.process = subprocess.Popen(cmd,
54 | universal_newlines=True,
55 | env=env or os.environ,
56 | cwd=cwd,
57 | stderr=subprocess.STDOUT,
58 | stdout=subprocess.PIPE,
59 | bufsize=1,
60 | close_fds='posix' in sys.builtin_module_names)
61 | return self.process
62 |
63 | @staticmethod
64 | def call(cmd, env=None, cwd=None):
65 | logging.info('Calling command: {}'.format(cmd))
66 | return subprocess.check_call(cmd, env=env, cwd=cwd)
67 |
68 | def wait(self, timeout=600):
69 | if timeout:
70 | end_time = time.time() + timeout
71 | interval = min(timeout / 1000.0, .25)
72 | while True:
73 | result = self.process.poll()
74 | if result is not None:
75 | return result
76 | if time.time() >= end_time:
77 | break
78 | time.sleep(interval)
79 | self.stop()
80 | self.process.wait()
81 |
82 | @staticmethod
83 | def set_environ(context=None):
84 | env = os.environ
85 | if context is None:
86 | return env
87 | for key, val in context.items():
88 | if isinstance(val, dict):
89 | env[key] = ','.join('{!s}={!r}'.format(k, v) for (k, v) in val.items())
90 | else:
91 | env[key] = val
92 | return env
93 |
94 | def is_running(self):
95 | if self.process is None:
96 | return False
97 | if self.process.poll() is not None:
98 | return False
99 | return True
100 |
101 | def stop(self):
102 | if self.process:
103 | try:
104 | self.process.terminate()
105 | self.process.kill()
106 | except Exception as e:
107 | logging.error(e)
108 |
--------------------------------------------------------------------------------
/core/plugins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MozillaSecurity/quokka/8b9c64b51cd34f4ea8842432df1ecb99d2aba7dc/core/plugins/__init__.py
--------------------------------------------------------------------------------
/core/plugins/command.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 | import shlex
7 | import logging
8 |
9 | from ..plugin import PluginProcess, PluginException
10 |
11 |
12 | class ConsoleApplication(PluginProcess):
13 |
14 | def __init__(self, quokka):
15 | super(ConsoleApplication, self).__init__()
16 | self.quokka = quokka
17 |
18 | def start(self):
19 | binary = self.quokka.plugin.kargs.get('binary')
20 | if not binary or not os.path.exists(binary):
21 | raise PluginException('%s not found.' % binary)
22 |
23 | params = self.quokka.plugin.kargs.get('params', '')
24 | cmd = [binary] + shlex.split(params)
25 |
26 | self.process = self.open(cmd, self.set_environ(self.quokka.get('environ')))
27 |
28 | def stop(self):
29 | if not self.process:
30 | return
31 | try:
32 | self.process.terminate()
33 | self.process.kill()
34 | except Exception as msg:
35 | logging.error(msg)
36 |
--------------------------------------------------------------------------------
/core/plugins/firefox.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import os
6 | import shlex
7 | import shutil
8 | import logging
9 | import tempfile
10 |
11 | from ..plugin import PluginProcess, PluginException
12 |
13 |
14 | class FirefoxApplication(PluginProcess):
15 |
16 | def __init__(self, quokka):
17 | super(FirefoxApplication, self).__init__()
18 | self.quokka = quokka
19 |
20 | self.profile_path = ''
21 |
22 | def start(self):
23 | binary = self.quokka.plugin.kargs.get('binary')
24 | if not binary or not os.path.exists(binary):
25 | raise PluginException('%s not found.' % binary)
26 |
27 | params = self.quokka.plugin.kargs.get('params', '')
28 |
29 | environ = self.set_environ(self.quokka.get('environ'))
30 |
31 | prefs = self.quokka.plugin.kargs.get('prefs')
32 | if not prefs or not os.path.exists(prefs):
33 | raise PluginException('No preferences provided.')
34 |
35 | self.profile_path = tempfile.mkdtemp()
36 | profile_name = os.path.basename(self.profile_path)
37 | cmd = [binary, '-no-remote', '-CreateProfile', '%s %s' % (profile_name, self.profile_path)]
38 | self.call(cmd, environ)
39 |
40 | shutil.copyfile(prefs, os.path.join(self.profile_path, 'prefs.js'))
41 | print(prefs)
42 | cmd = [binary, '-P', profile_name] + shlex.split(params)
43 |
44 | self.process = self.open(cmd, environ)
45 |
46 | def stop(self):
47 | if os.path.isdir(self.profile_path):
48 | logging.info("Deleting Firefox profile path: %s" % self.profile_path)
49 | try:
50 | shutil.rmtree(self.profile_path)
51 | except Exception as msg:
52 | logging.error(msg)
53 | if self.process:
54 | try:
55 | self.process.terminate()
56 | self.process.kill()
57 | except Exception as msg:
58 | logging.error(msg)
59 |
--------------------------------------------------------------------------------
/core/quokka.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import logging
6 |
7 | from .plugin import PluginException
8 |
9 |
10 | class QuokkaException(Exception):
11 | """
12 | Unrecoverable error in Quokka.
13 | """
14 | pass
15 |
16 |
17 | class Quokka(object):
18 | """
19 | Quokka observer class.
20 | """
21 |
22 | def __init__(self, conf):
23 | self.conf = conf
24 | self.monitors = []
25 | self.loggers = []
26 | self.plugin = None
27 |
28 | @staticmethod
29 | def import_plugin_class(module_path):
30 | """Import a plugin class.
31 |
32 | :param module_path: Path to Python class
33 | :return: Class object
34 | """
35 | module_path, class_name = module_path.rsplit(".", 1)
36 | logging.debug("Importing '%s' from '%s'" % (class_name, module_path))
37 | try:
38 | module = __import__(module_path, fromlist=[""])
39 | except ImportError as msg:
40 | raise PluginException(msg)
41 | try:
42 | return getattr(module, class_name)
43 | except AttributeError as msg:
44 | raise PluginException(msg)
45 |
46 | def run_plugin(self):
47 | """Run a program which needs complex setup steps.
48 |
49 | :return: Exit code of the target process.
50 | """
51 | try:
52 | plugin_class = self.import_plugin_class('core.plugins.' + self.conf.plugin_class)
53 | except PluginException as msg:
54 | raise QuokkaException("Plugin initialization failed: %s" % msg)
55 |
56 | self.plugin = plugin_class(self.conf.quokka)
57 | try:
58 | self.plugin.start()
59 | except PluginException as msg:
60 | raise QuokkaException(msg)
61 |
62 | self.attach_monitors(self.plugin, self.conf.monitors)
63 | self.attach_loggers(self.conf.loggers)
64 |
65 | self.plugin.process.wait()
66 |
67 | self.detect_faults()
68 |
69 | return self.plugin.process.returncode
70 |
71 | def stop_plugin(self):
72 | """Initiate a plugin's shutdown routines by calling its stop function.
73 |
74 | :return: None
75 | """
76 | if not self.plugin:
77 | logging.info("Plugin did not start.")
78 | return
79 | if not self.plugin.is_running():
80 | logging.info("Plugin process exited prior with exit code: %d" % self.plugin.process.returncode)
81 | return
82 | try:
83 | self.plugin.stop()
84 | except PluginException as msg:
85 | raise QuokkaException(msg)
86 |
87 | def attach_monitors(self, plugin, monitors):
88 | """Attach a list of monitors and listeners to observe the target process for faults.
89 |
90 | :param plugin: Plugin instance
91 | :param monitors: List of monitors
92 | :return: None
93 | """
94 | for monitor in monitors:
95 | monitor_class = monitor.get("class")
96 | monitor_kargs = monitor.get("kargs")
97 | monitor_listeners = monitor.get("listeners")
98 | logging.info("Attaching monitor '%s'" % monitor_class)
99 | monitor_class = self.import_plugin_class('core.monitors.' + monitor_class)
100 | if monitor_class.MONITOR_NAME == "ConsoleMonitor":
101 | monitor_instance = monitor_class(plugin.process, *monitor_kargs)
102 | elif monitor_class.MONITOR_NAME == "WebSocketMonitor":
103 | pass
104 | else:
105 | logging.warning("Unsupported monitor: %s" % monitor_class)
106 | continue
107 | for listener in monitor_listeners:
108 | listener_class = listener.get("class")
109 | listener_kargs = listener.get("kargs")
110 | logging.info("Attaching listener '%s'" % listener_class)
111 | listener_class = self.import_plugin_class('core.listeners.' + listener_class)
112 | listener_instance = listener_class(*listener_kargs)
113 | monitor_instance.add_listener(listener_instance)
114 | monitor_instance.daemon = True
115 | monitor_instance.start()
116 | self.monitors.append(monitor_instance)
117 |
118 | def attach_loggers(self, loggers):
119 | """Attach a list of loggers to bucket the monitors found faults.
120 |
121 | :param loggers: A list of loggers
122 | :return: None
123 | """
124 | for logger in loggers:
125 | logger_class = logger.get("class")
126 | logger_kargs = logger.get("kargs")
127 | logging.info("Attaching logger '%s'" % logger_class)
128 | logger_class = self.import_plugin_class('core.loggers.' + logger_class)
129 | logger = logger_class(**logger_kargs)
130 | self.loggers.append(logger)
131 |
132 | def detect_faults(self):
133 | """Observe each attached monitor for faults and add each fault to the attached loggers.
134 |
135 | :return: None
136 | """
137 | for monitor in self.monitors:
138 | if monitor.detected_fault():
139 | monitor_data = monitor.get_data()
140 | for logger in self.loggers:
141 | logger.add_to_bucket(monitor_data)
142 | for logger in self.loggers:
143 | logger.add_fault()
144 |
--------------------------------------------------------------------------------
/core/websocket.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | import base64
6 | import email.message
7 | import email.parser
8 | import hashlib
9 | import logging
10 | import socket
11 | import struct
12 | import sys
13 | try:
14 | # python 3
15 | from socketserver import BaseRequestHandler
16 | except ImportError:
17 | # python 2
18 | from SocketServer import BaseRequestHandler
19 |
20 |
21 | class BaseWebSocketHandler(BaseRequestHandler):
22 | _opcodes = {
23 | 0: 'continue',
24 | 1: 'text',
25 | 2: 'binary',
26 | 8: 'close',
27 | 9: 'ping',
28 | 10: 'pong'
29 | }
30 |
31 | def handle(self):
32 | self.request.settimeout(0.01)
33 | str_t = str if sys.version_info[0] == 3 else lambda a, b: str(a).encode(b)
34 | while not self.should_close():
35 | try:
36 | request, headers = str_t(self.request.recv(1024), 'ascii').split('\r\n', 1)
37 | break
38 | except socket.timeout:
39 | #time.sleep(0.01)
40 | #print 'timeout 29'
41 | continue
42 | headers = email.parser.HeaderParser().parsestr(headers)
43 | # TODO(jschwartzentruber): validate request/headers
44 | hresponse = hashlib.sha1(headers['sec-websocket-key'].encode('ascii'))
45 | hresponse.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
46 | resp = email.message.Message()
47 | resp.add_header('Upgrade', 'websocket')
48 | resp.add_header('Connection', 'Upgrade')
49 | resp.add_header('Sec-WebSocket-Accept', str_t(base64.b64encode(hresponse.digest()), 'ascii'))
50 | resp = resp.as_string(unixfrom=False).replace('\n', '\r\n')
51 | self.request.sendall('HTTP/1.1 101 Switching Protocols\r\n{}'.format(resp).encode('ascii'))
52 | self.open()
53 | buf = None
54 | buf_op = None
55 | try:
56 | while not self.should_close():
57 | try:
58 | data = struct.unpack('BB', self.request.recv(2))
59 | except socket.timeout:
60 | # no data
61 | #time.sleep(0.01)
62 | #print 'timeout 51'
63 | continue
64 | except struct.error:
65 | break # chrome doesn't send a close-frame
66 | fin, mask = bool(data[0] & 0x80), bool(data[1] & 0x80)
67 | opcode = self._opcodes[data[0] & 0xF]
68 | if opcode == 'close':
69 | break
70 | elif opcode == 'pong':
71 | self.on_pong()
72 | continue
73 | length = data[1] & 0x7F
74 | if length == 126:
75 | length = struct.unpack('!H', self.request.recv(2))[0]
76 | elif length == 127:
77 | length = struct.unpack('!Q', self.request.recv(8))[0]
78 | mask = bytearray(self.request.recv(4)) if mask else None
79 | data = bytearray(self.request.recv(length))
80 | if mask is not None:
81 | data = bytearray((b ^ mask[i % 4]) for (i, b) in enumerate(data))
82 | if opcode == 'continue':
83 | assert buf is not None
84 | opcode = buf_op
85 | elif opcode == 'ping':
86 | self._send(10, data)
87 | continue
88 | elif buf is not None:
89 | logging.warning('Received a new frame while waiting for another to finish, '
90 | 'discarding {} bytes of {}'.format(len(buf), buf_op))
91 | buf = buf_op = None
92 | if opcode == 'text':
93 | data = str_t(data, 'utf8')
94 | elif opcode != 'binary':
95 | logging.warning('Unknown websocket opcode {}'.format(opcode))
96 | continue
97 | if buf is None:
98 | buf = data
99 | buf_op = opcode
100 | else:
101 | buf += data
102 | if fin:
103 | self.on_message(buf)
104 | buf = buf_op = None
105 | finally:
106 | self.on_close()
107 |
108 | def finish(self):
109 | pass
110 |
111 | def _send(self, opcode, data):
112 | length = len(data)
113 | out = bytearray()
114 | out.append(0x80 | opcode)
115 | if length <= 125:
116 | out.append(length)
117 | elif length <= 65535:
118 | out.append(126)
119 | out.extend(struct.pack('!H', length))
120 | else:
121 | out.append(127)
122 | out.extend(struct.pack('!Q', length))
123 | if length:
124 | out.extend(data)
125 | self.request.sendall(out)
126 |
127 | # Below is the partial API from tornado.websocket.WebSocketHandler
128 | def ping(self):
129 | self._send(9, '')
130 |
131 | def should_close(self):
132 | """When this returns true, the message loop will exit."""
133 | return False
134 |
135 | def write_message(self, message, binary=False):
136 | if binary:
137 | self._send(2, message)
138 | else:
139 | self._send(1, message.encode('utf8'))
140 |
141 | # Event handlers
142 | def on_pong(self):
143 | pass
144 |
145 | def open(self):
146 | pass
147 |
148 | def on_close(self):
149 | pass
150 |
151 | def on_message(self, message):
152 | raise NotImplementedError('Required method on_message() not implemented.')
153 |
--------------------------------------------------------------------------------
/quokka.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | # This Source Code Form is subject to the terms of the Mozilla Public
4 | # License, v. 2.0. If a copy of the MPL was not distributed with this
5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | """
7 | Quokka is a utility to run and monitor a to fuzzed application.
8 | """
9 | import os
10 | import sys
11 | import logging
12 | import argparse
13 |
14 | from core.quokka import Quokka, QuokkaException
15 | from core.config import QuokkaConf
16 |
17 |
18 | class QuokkaCommandLine(object):
19 | """
20 | Command-line interface for Quokka
21 | """
22 | HOME = os.path.dirname(os.path.abspath(__file__))
23 | VERSION = 0.1
24 | CONFIG_PATH = os.path.relpath(os.path.join(HOME, 'configs'))
25 | QUOKKA_CONFIG = os.path.join(CONFIG_PATH, 'quokka.json')
26 |
27 | def parse_args(self):
28 | parser = argparse.ArgumentParser(description='Quokka Runtime',
29 | prog=__file__,
30 | add_help=False,
31 | formatter_class=argparse.ArgumentDefaultsHelpFormatter,
32 | epilog='The exit status is 0 for non-failures and 1 for failures.')
33 |
34 | m = parser.add_argument_group('Mandatory Arguments')
35 | g = m.add_mutually_exclusive_group(required=True)
36 | g.add_argument('-plugin', metavar='file', type=argparse.FileType(), help='Run an application.')
37 |
38 | o = parser.add_argument_group('Optional Arguments')
39 | o.add_argument('-quokka', metavar='file', type=argparse.FileType(), default=self.QUOKKA_CONFIG,
40 | help='Quokka configuration')
41 | o.add_argument('-conf-args', metavar='k=v', nargs='+', type=str, help='Add/edit configuration properties.')
42 | o.add_argument('-conf-vars', metavar='k=v', nargs='+', type=str, help='Subsitute configuration variables.')
43 | o.add_argument('-list-conf-vars', action='store_true', help='List used configuration variables.')
44 | o.add_argument('-verbosity', metavar='{1..5}', default=2, type=int, choices=list(range(1, 6, 1)),
45 | help='Level of verbosity for logging module.')
46 | o.add_argument('-h', '-help', '--help', action='help', help=argparse.SUPPRESS)
47 | o.add_argument('-version', action='version', version='%(prog)s {}'.format(self.VERSION), help=argparse.SUPPRESS)
48 |
49 | return parser.parse_args()
50 |
51 | @staticmethod
52 | def pair_to_dict(args):
53 | return dict(kv.split('=', 1) for kv in args)
54 |
55 | def main(self):
56 | args = self.parse_args()
57 |
58 | logging.basicConfig(format='[Quokka] %(asctime)s %(levelname)s: %(message)s',
59 | level=args.verbosity * 10,
60 | datefmt='%Y-%m-%d %H:%M:%S')
61 |
62 | if args.list_conf_vars:
63 | conf_vars = []
64 | try:
65 | conf_vars.extend(QuokkaConf.list_conf_vars(args.quokka.read()))
66 | if args.plugin:
67 | conf_vars.extend(QuokkaConf.list_conf_vars(args.plugin.read()))
68 | except QuokkaException as msg:
69 | logging.error(msg)
70 | return 1
71 | if len(conf_vars):
72 | logging.info("List of available configuration variables:")
73 | for v in conf_vars:
74 | logging.info('\t%r', v)
75 | return 0
76 |
77 | logging.info('Loading Quokka configuration from %s' % args.quokka.name)
78 | try:
79 | quokka_conf = args.quokka.read()
80 | if args.conf_vars:
81 | quokka_conf = QuokkaConf.set_conf_vars(quokka_conf, self.pair_to_dict(args.conf_vars))
82 | quokka_conf = QuokkaConf(quokka_conf)
83 | except QuokkaException as msg:
84 | logging.error(msg)
85 | return 1
86 |
87 | if args.plugin:
88 | logging.info('Loading plugin configuration from %s' % args.plugin.name)
89 | try:
90 | plugin_conf = args.plugin.read()
91 | if args.conf_vars:
92 | plugin_conf = QuokkaConf.set_conf_vars(plugin_conf, self.pair_to_dict(args.conf_vars))
93 | quokka_conf.add_plugin_conf(plugin_conf)
94 | except QuokkaException as msg:
95 | logging.error(msg)
96 | return 1
97 |
98 | if args.conf_args:
99 | conf_args = self.pair_to_dict(args.conf_args)
100 | logging.info('Updating configuration with: %r' % conf_args)
101 | for k, v in conf_args.items():
102 | if k in quokka_conf.quokka:
103 | quokka_conf.quokka[k] = v
104 |
105 | logging.debug(quokka_conf.quokka)
106 |
107 | if args.plugin:
108 | quokka = Quokka(quokka_conf)
109 | try:
110 | quokka.run_plugin()
111 | except QuokkaException as msg:
112 | logging.error(msg)
113 | return 1
114 | except KeyboardInterrupt:
115 | print('')
116 | logging.info("Caught SIGINT - Aborting.")
117 | return 0
118 | finally:
119 | logging.info("Initiating plugin shutdown routines.")
120 | try:
121 | quokka.stop_plugin()
122 | except QuokkaException as msg:
123 | logging.error(msg)
124 | return 1
125 |
126 | return 0
127 |
128 |
129 | if __name__ == '__main__':
130 | sys.exit(QuokkaCommandLine().main())
131 |
--------------------------------------------------------------------------------