├── .gitignore ├── LICENSE ├── README.md ├── client ├── css │ ├── angular-csp.css │ ├── app.css │ ├── bootstrap.css │ ├── codemirror-mdn.css │ ├── codemirror.css │ ├── dashboard.css │ ├── font-awesome.css │ └── prettify.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── index.html ├── js │ ├── experimentctrl.js │ ├── lib │ │ ├── acorn.js │ │ ├── acorn_loose.js │ │ ├── angular-1.2.13.min.js │ │ ├── codemirror-compressed.js │ │ ├── codemirror.js │ │ ├── codemirror_javascript_mode.js │ │ ├── jquery-2.1.0.min.js │ │ ├── jszip.min.js │ │ ├── localforage.min.js │ │ ├── ui-bootstrap-tpls-0.3.0.js │ │ ├── utils.js │ │ └── walk.js │ ├── main.js │ ├── prettify.js │ ├── rulesctrl.js │ ├── scanctrl.js │ ├── scanservice.js │ └── scanworker.js └── rules.readme.md ├── common ├── rules.json ├── scan.js └── template_rules.json ├── package.json ├── scanner.js ├── server.js ├── stackato.yml └── tests ├── advanced.html ├── cases ├── CustomEvent.js ├── action.js ├── addEventListener.js ├── addIdleObserver.js ├── createContextualFragment.js ├── crypto.generateCRMFRequest.js ├── data.js ├── document.write.js ├── document.writeln.js ├── escapeHTML.js ├── eval.js ├── geolocation.js ├── getDeviceStorage.js ├── href.js ├── indexedDB.js ├── innerhtml.js ├── localStorage.js ├── message.js ├── moz │ └── moz.js ├── newFunction.js ├── outerHTML.js ├── parseFromString.js ├── production_ruletests.js ├── sessionStorage.js ├── setInterval.js ├── setTimeout.js ├── src.js ├── test_ruletests.js └── window.open.js ├── css └── mocha.css ├── index.html └── js ├── chai.js ├── loadrules.js ├── main.js └── mocha.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scanjs 2 | ====== 3 | 4 | - Static analysis tool for javascript codebases. Scanjs uses Acorn to convert sources to AST, then walks AST looking for patterns. 5 | - Works on both client and server side 6 | 7 | Client-side instructions 8 | ------------------------ 9 | - ```git clone https://github.com/mozilla/scanjs.git``` 10 | - ```node server.js``` 11 | - Navigate to scanjs/client/ or see our [example page](http://mozilla.github.io/scanjs/client/) 12 | 13 | Server-side instructions 14 | ------------------------ 15 | - Install [node.js](http://nodejs.org/) 16 | - ```git clone https://github.com/mozilla/scanjs.git``` 17 | - ```cd scanjs``` 18 | - ```npm install``` 19 | - ```node scanner.js -t DIRECTORY_PATH``` 20 | 21 | Testing instructions 22 | ------------------------ 23 | We use the mocha testing framework. 24 | ```node server.js``` 25 | ```http://127.0.0.1:4000/tests/``` 26 | 27 | To add tests, create a new file in ```/tests/cases/``` and following the naming 28 | convention, which should be obvious. For example, our rule named .innerHTML 29 | lives in ```/tests/cases/innerhtml.js```. 30 | 31 | From there, add the new test case to ```/tests/index.html```. In our 32 | example, that would involve adding a ``````. 33 | -------------------------------------------------------------------------------- /client/css/angular-csp.css: -------------------------------------------------------------------------------- 1 | /* Include this file in your html if you are using the CSP mode. */ 2 | 3 | @charset "UTF-8"; 4 | 5 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], 6 | .ng-cloak, .x-ng-cloak, 7 | .ng-hide { 8 | display: none !important; 9 | } 10 | 11 | ng\:form { 12 | display: block; 13 | } 14 | 15 | .ng-animate-block-transitions { 16 | transition:0s all!important; 17 | -webkit-transition:0s all!important; 18 | } 19 | -------------------------------------------------------------------------------- /client/css/app.css: -------------------------------------------------------------------------------- 1 | @import url("bootstrap.css"); 2 | @import url("dashboard.css"); 3 | @import url("codemirror.css"); 4 | @import url("codemirror-mdn.css"); 5 | @import url("prettify.css"); 6 | @import url("font-awesome.css"); 7 | 8 | html, body { 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | pre.prettyprint{ 14 | width: auto; 15 | max-width: 600px; 16 | overflow: auto; 17 | font-size: x-small; 18 | } 19 | 20 | .navbar { 21 | background: #4D4E53; 22 | } 23 | 24 | .nav > li > a:hover, 25 | .nav > li > a:focus { 26 | background-color: #0095DD; 27 | } 28 | 29 | a { 30 | cursor:pointer; 31 | } 32 | 33 | .sidebar { 34 | background-color: #EAEFF2; 35 | } 36 | 37 | .jumbotron { 38 | background: #D4DDE4; 39 | } 40 | 41 | .table { 42 | border: 1px solid #EAEFF2; 43 | } 44 | 45 | .table-striped > tbody > tr:nth-child(odd) > td, 46 | .table-striped > tbody > tr:nth-child(odd) > th { 47 | background-color: #EAEFF2; 48 | } 49 | 50 | .sidebar-inputfiles{ 51 | font-size: x-small; 52 | } 53 | 54 | .CodeMirror { 55 | border: 1px solid #EAEFF2; 56 | height:500px; 57 | } 58 | 59 | .badge { 60 | margin: 3px 3px 3px 3px; 61 | } 62 | 63 | @-webkit-keyframes rotating { 64 | 0% { 65 | -webkit-transform: rotate(0deg); 66 | color: #333; 67 | } 68 | 25% { 69 | -webkit-transform: rotate(90deg); 70 | color: #70706F; 71 | } 72 | 50% { 73 | -webkit-transform: rotate(180deg); 74 | color: #ADADAB; 75 | } 76 | 75% { 77 | -webkit-transform: rotate(270deg); 78 | color: #EAEAE7; 79 | } 80 | 100% { 81 | -webkit-transform: rotate(360deg); 82 | color: #333; 83 | } 84 | } 85 | 86 | @keyframes rotating { 87 | 0% { 88 | transform: rotate(0deg); 89 | color: #333; 90 | } 91 | 25% { 92 | transform: rotate(90deg); 93 | color: #70706F; 94 | } 95 | 50% { 96 | transform: rotate(180deg); 97 | color: #ADADAB; 98 | } 99 | 75% { 100 | transform: rotate(270deg); 101 | color: #EAEAE7; 102 | } 103 | 100% { 104 | transform: rotate(360deg); 105 | color: #333; 106 | } 107 | } 108 | 109 | .rotating { 110 | margin: 5px 3px 0 3px; 111 | -webkit-animation: rotating 1s linear infinite; 112 | animation: rotating 1s linear infinite; 113 | } 114 | -------------------------------------------------------------------------------- /client/css/codemirror-mdn.css: -------------------------------------------------------------------------------- 1 | /* 2 | MDN-LIKE Theme - Mozilla 3 | Ported to CodeMirror by Peter Kroon 4 | Report bugs/issues here: https://github.com/marijnh/CodeMirror/issues 5 | GitHub: @peterkroon 6 | 7 | The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation 8 | 9 | */ 10 | .cm-s-mdn-like.CodeMirror { color: #999; font-family: monospace; background-color: #fff; } 11 | .cm-s-mdn-like .CodeMirror-selected { background: #cfc !important; } 12 | 13 | .cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 6px solid rgba(0,83,159,0.65); color: #333; } 14 | .cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; margin-left: 3px; } 15 | div.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; } 16 | 17 | .cm-s-mdn-like .cm-keyword { color: #6262FF; } 18 | .cm-s-mdn-like .cm-atom { color: #F90; } 19 | .cm-s-mdn-like .cm-number { color: #ca7841; } 20 | .cm-s-mdn-like .cm-def { color: #8DA6CE; } 21 | .cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; } 22 | .cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def { color: #07a; } 23 | 24 | .cm-s-mdn-like .cm-variable { color: #07a; } 25 | .cm-s-mdn-like .cm-property { color: #905; } 26 | .cm-s-mdn-like .cm-qualifier { color: #690; } 27 | 28 | .cm-s-mdn-like .cm-operator { color: #cda869; } 29 | .cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; } 30 | .cm-s-mdn-like .cm-string { color:#07a; font-style:italic; } 31 | .cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/ 32 | .cm-s-mdn-like .cm-meta { color: #000; } /*?*/ 33 | .cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/ 34 | .cm-s-mdn-like .cm-tag { color: #997643; } 35 | .cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/ 36 | .cm-s-mdn-like .cm-header { color: #FF6400; } 37 | .cm-s-mdn-like .cm-hr { color: #AEAEAE; } 38 | .cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } 39 | .cm-s-mdn-like .cm-error { border-bottom: 1px solid red; } 40 | 41 | div.cm-s-mdn-like .CodeMirror-activeline-background {background: #efefff;} 42 | div.cm-s-mdn-like span.CodeMirror-matchingbracket {outline:1px solid grey; color: inherit;} 43 | 44 | .cm-s-mdn-like.CodeMirror { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFcAAAAyCAYAAAAp8UeFAAAHvklEQVR42s2b63bcNgyEQZCSHCdt2vd/0tWF7I+Q6XgMXiTtuvU5Pl57ZQKkKHzEAOtF5KeIJBGJ8uvL599FRFREZhFx8DeXv8trn68RuGaC8TRfo3SNp9dlDDHedyLyTUTeRWStXKPZrjtpZxaRw5hPqozRs1N8/enzIiQRWcCgy4MUA0f+XWliDhyL8Lfyvx7ei/Ae3iQFHyw7U/59pQVIMEEPEz0G7XiwdRjzSfC3UTtz9vchIntxvry5iMgfIhJoEflOz2CQr3F5h/HfeFe+GTdLaKcu9L8LTeQb/R/7GgbsfKedyNdoHsN31uRPWrfZ5wsj/NzzRQHuToIdU3ahwnsKPxXCjJITuOsi7XLc7SG/v5GdALs7wf8JjTFiB5+QvTEfRyGOfX3Lrx8wxyQi3sNq46O7QahQiCsRFgqddjBouVEHOKDgXAQHD9gJCr5sMKkEdjwsarG/ww3BMHBU7OBjXnzdyY7SfCxf5/z6ATccrwlKuwC/jhznnPF4CgVzhhVf4xp2EixcBActO75iZ8/fM9zAs2OMzKdslgXWJ9XG8PQoOAMA5fGcsvORgv0doBXyHrCwfLJAOwo71QLNkb8n2Pl6EWiR7OCibtkPaz4Kc/0NNAze2gju3zOwekALDaCFPI5vjPFmgGY5AZqyGEvH1x7QfIb8YtxMnA/b+QQ0aQDAwc6JMFg8CbQZ4qoYEEHbRwNojuK3EHwd7VALSgq+MNDKzfT58T8qdpADrgW0GmgcAS1lhzztJmkAzcPNOQbsWEALBDSlMKUG0Eq4CLAQWvEVQ9WU57gZJwZtgPO3r9oBTQ9WO8TjqXINx8R0EYpiZEUWOF3FxkbJkgU9B2f41YBrIj5ZfsQa0M5kTgiAAqM3ShXLgu8XMqcrQBvJ0CL5pnTsfMB13oB8athpAq2XOQmcGmoACCLydx7nToa23ATaSIY2ichfOdPTGxlasXMLaL0MLZAOwAKIM+y8CmicobGdCcbbK9DzN+yYGVoNNI5iUKTMyYOjPse4A8SM1MmcXgU0toOq1yO/v8FOxlASyc7TgeYaAMBJHcY1CcCwGI/TK4AmDbDyKYBBtFUkRwto8gygiQEaByFgJ00BH2M8JWwQS1nafDXQCidWyOI8AcjDCSjCLk8ngObuAm3JAHAdubAmOaK06V8MNEsKPJOhobSprwQa6gD7DclRQdqcwL4zxqgBrQcabUiBLclRDKAlWp+etPkBaNMA0AKlrHwTdEByZAA4GM+SNluSY6wAzcMNewxmgig5Ks0nkrSpBvSaQHMdKTBAnLojOdYyGpQ254602ZILPdTD1hdlggdIm74jbTp8vDwF5ZYUeLWGJpWsh6XNyXgcYwVoJQTEhhTYkxzZjiU5npU2TaB979TQehlaAVq4kaGpiPwwwLkYUuBbQwocyQTv1tA0+1UFWoJF3iv1oq+qoSk8EQdJmwHkziIF7oOZk14EGitibAdjLYYK78H5vZOhtWpoI0ATGHs0Q8OMb4Ey+2bU2UYztCtA0wFAs7TplGLRVQCcqaFdGSPCeTI1QNIC52iWNzof6Uib7xjEp07mNNoUYmVosVItHrHzRlLgBn9LFyRHaQCtVUMbtTNhoXWiTOO9k/V8BdAc1Oq0ArSQs6/5SU0hckNy9NnXqQY0PGYo5dWJ7nINaN6o958FWin27aBaWRka1r5myvLOAm0j30eBJqCxHLReVclxhxOEN2JfDWjxBtAC7MIH1fVaGdoOp4qJYDgKtKPSFNID2gSnGldrCqkFZ+5UeQXQBIRrSwocbdZYQT/2LwRahBPBXoHrB8nxaGROST62DKUbQOMMzZIC9abkuELfQzQALWTnDNAm8KHWFOJgJ5+SHIvTPcmx1xQyZRhNL5Qci689aXMEaN/uNIWkEwDAvFpOZmgsBaaGnbs1NPa1Jm32gBZAIh1pCtG7TSH4aE0y1uVY4uqoFPisGlpP2rSA5qTecWn5agK6BzSpgAyD+wFaqhnYoSZ1Vwr8CmlTQbrcO3ZaX0NAEyMbYaAlyquFoLKK3SPby9CeVUPThrSJmkCAE0CrKUQadi4DrdSlWhmah0YL9z9vClH59YGbHx1J8VZTyAjQepJjmXwAKTDQI3omc3p1U4gDUf6RfcdYfrUp5ClAi2J3Ba6UOXGo+K+bQrjjssitG2SJzshaLwMtXgRagUNpYYoVkMSBLM+9GGiJZMvduG6DRZ4qc04DMPtQQxOjEtACmhO7K1AbNbQDEggZyJwscFpAGwENhoBeUwh3bWolhe8BTYVKxQEWrSUn/uhcM5KhvUu/+eQu0Lzhi+VrK0PrZZNDQKs9cpYUuFYgMVpD4/NxenJTiMCNqdUEUf1qZWjppLT5qSkkUZbCwkbZMSuVnu80hfSkzRbQeqCZSAh6huR4VtoM2gHAlLf72smuWgE+VV7XpE25Ab2WFDgyhnSuKbs4GuGzCjR+tIoUuMFg3kgcWKLTwRqanJQ2W00hAsenfaApRC42hbCvK1SlE0HtE9BGgneJO+ELamitD1YjjOYnNYVcraGhtKkW0EqVVeDx733I2NH581k1NNxNLG0i0IJ8/NjVaOZ0tYZ2Vtr0Xv7tPV3hkWp9EFkgS/J0vosngTaSoaG06WHi+xObQkaAdlbanP8B2+2l0f90LmUAAAAASUVORK5CYII=); } 45 | -------------------------------------------------------------------------------- /client/css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | } 8 | .CodeMirror-scroll { 9 | /* Set scrolling behaviour here */ 10 | overflow: auto; 11 | } 12 | 13 | /* PADDING */ 14 | 15 | .CodeMirror-lines { 16 | padding: 4px 0; /* Vertical padding around content */ 17 | } 18 | .CodeMirror pre { 19 | padding: 0 4px; /* Horizontal padding of content */ 20 | } 21 | 22 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 23 | background-color: white; /* The little square between H and V scrollbars */ 24 | } 25 | 26 | /* GUTTER */ 27 | 28 | .CodeMirror-gutters { 29 | border-right: 1px solid #ddd; 30 | background-color: #f7f7f7; 31 | white-space: nowrap; 32 | } 33 | .CodeMirror-linenumbers {} 34 | .CodeMirror-linenumber { 35 | padding: 0 3px 0 5px; 36 | min-width: 20px; 37 | text-align: right; 38 | color: #999; 39 | -moz-box-sizing: content-box; 40 | box-sizing: content-box; 41 | } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror div.CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | z-index: 3; 48 | } 49 | /* Shown when moving in bi-directional text */ 50 | .CodeMirror div.CodeMirror-secondarycursor { 51 | border-left: 1px solid silver; 52 | } 53 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { 54 | width: auto; 55 | border: 0; 56 | background: #7e7; 57 | z-index: 1; 58 | } 59 | /* Can style cursor different in overwrite (non-insert) mode */ 60 | .CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} 61 | 62 | .cm-tab { display: inline-block; } 63 | 64 | .CodeMirror-ruler { 65 | border-left: 1px solid #ccc; 66 | position: absolute; 67 | } 68 | 69 | /* DEFAULT THEME */ 70 | 71 | .cm-s-default .cm-keyword {color: #708;} 72 | .cm-s-default .cm-atom {color: #219;} 73 | .cm-s-default .cm-number {color: #164;} 74 | .cm-s-default .cm-def {color: #00f;} 75 | .cm-s-default .cm-variable {color: black;} 76 | .cm-s-default .cm-variable-2 {color: #05a;} 77 | .cm-s-default .cm-variable-3 {color: #085;} 78 | .cm-s-default .cm-property {color: black;} 79 | .cm-s-default .cm-operator {color: black;} 80 | .cm-s-default .cm-comment {color: #a50;} 81 | .cm-s-default .cm-string {color: #a11;} 82 | .cm-s-default .cm-string-2 {color: #f50;} 83 | .cm-s-default .cm-meta {color: #555;} 84 | .cm-s-default .cm-qualifier {color: #555;} 85 | .cm-s-default .cm-builtin {color: #30a;} 86 | .cm-s-default .cm-bracket {color: #997;} 87 | .cm-s-default .cm-tag {color: #170;} 88 | .cm-s-default .cm-attribute {color: #00c;} 89 | .cm-s-default .cm-header {color: blue;} 90 | .cm-s-default .cm-quote {color: #090;} 91 | .cm-s-default .cm-hr {color: #999;} 92 | .cm-s-default .cm-link {color: #00c;} 93 | 94 | .cm-negative {color: #d44;} 95 | .cm-positive {color: #292;} 96 | .cm-header, .cm-strong {font-weight: bold;} 97 | .cm-em {font-style: italic;} 98 | .cm-link {text-decoration: underline;} 99 | 100 | .cm-s-default .cm-error {color: #f00;} 101 | .cm-invalidchar {color: #f00;} 102 | 103 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 104 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 105 | .CodeMirror-activeline-background {background: #e8f2ff;} 106 | 107 | /* STOP */ 108 | 109 | /* The rest of this file contains styles related to the mechanics of 110 | the editor. You probably shouldn't touch them. */ 111 | 112 | .CodeMirror { 113 | line-height: 1; 114 | position: relative; 115 | overflow: hidden; 116 | background: white; 117 | color: black; 118 | } 119 | 120 | .CodeMirror-scroll { 121 | /* 30px is the magic margin used to hide the element's real scrollbars */ 122 | /* See overflow: hidden in .CodeMirror */ 123 | margin-bottom: -30px; margin-right: -30px; 124 | padding-bottom: 30px; 125 | height: 100%; 126 | outline: none; /* Prevent dragging from highlighting the element */ 127 | position: relative; 128 | -moz-box-sizing: content-box; 129 | box-sizing: content-box; 130 | } 131 | .CodeMirror-sizer { 132 | position: relative; 133 | border-right: 30px solid transparent; 134 | -moz-box-sizing: content-box; 135 | box-sizing: content-box; 136 | } 137 | 138 | /* The fake, visible scrollbars. Used to force redraw during scrolling 139 | before actuall scrolling happens, thus preventing shaking and 140 | flickering artifacts. */ 141 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 142 | position: absolute; 143 | z-index: 6; 144 | display: none; 145 | } 146 | .CodeMirror-vscrollbar { 147 | right: 0; top: 0; 148 | overflow-x: hidden; 149 | overflow-y: scroll; 150 | } 151 | .CodeMirror-hscrollbar { 152 | bottom: 0; left: 0; 153 | overflow-y: hidden; 154 | overflow-x: scroll; 155 | } 156 | .CodeMirror-scrollbar-filler { 157 | right: 0; bottom: 0; 158 | } 159 | .CodeMirror-gutter-filler { 160 | left: 0; bottom: 0; 161 | } 162 | 163 | .CodeMirror-gutters { 164 | position: absolute; left: 0; top: 0; 165 | padding-bottom: 30px; 166 | z-index: 3; 167 | } 168 | .CodeMirror-gutter { 169 | white-space: normal; 170 | height: 100%; 171 | -moz-box-sizing: content-box; 172 | box-sizing: content-box; 173 | padding-bottom: 30px; 174 | margin-bottom: -32px; 175 | display: inline-block; 176 | /* Hack to make IE7 behave */ 177 | *zoom:1; 178 | *display:inline; 179 | } 180 | .CodeMirror-gutter-elt { 181 | position: absolute; 182 | cursor: default; 183 | z-index: 4; 184 | } 185 | 186 | .CodeMirror-lines { 187 | cursor: text; 188 | } 189 | .CodeMirror pre { 190 | /* Reset some styles that the rest of the page might have set */ 191 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 192 | border-width: 0; 193 | background: transparent; 194 | font-family: inherit; 195 | font-size: inherit; 196 | margin: 0; 197 | white-space: pre; 198 | word-wrap: normal; 199 | line-height: inherit; 200 | color: inherit; 201 | z-index: 2; 202 | position: relative; 203 | overflow: visible; 204 | } 205 | .CodeMirror-wrap pre { 206 | word-wrap: break-word; 207 | white-space: pre-wrap; 208 | word-break: normal; 209 | } 210 | 211 | .CodeMirror-linebackground { 212 | position: absolute; 213 | left: 0; right: 0; top: 0; bottom: 0; 214 | z-index: 0; 215 | } 216 | 217 | .CodeMirror-linewidget { 218 | position: relative; 219 | z-index: 2; 220 | overflow: auto; 221 | } 222 | 223 | .CodeMirror-widget {} 224 | 225 | .CodeMirror-wrap .CodeMirror-scroll { 226 | overflow-x: hidden; 227 | } 228 | 229 | .CodeMirror-measure { 230 | position: absolute; 231 | width: 100%; 232 | height: 0; 233 | overflow: hidden; 234 | visibility: hidden; 235 | } 236 | .CodeMirror-measure pre { position: static; } 237 | 238 | .CodeMirror div.CodeMirror-cursor { 239 | position: absolute; 240 | visibility: hidden; 241 | border-right: none; 242 | width: 0; 243 | } 244 | .CodeMirror-focused div.CodeMirror-cursor { 245 | visibility: visible; 246 | } 247 | 248 | .CodeMirror-selected { background: #d9d9d9; } 249 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 250 | 251 | .cm-searching { 252 | background: #ffa; 253 | background: rgba(255, 255, 0, .4); 254 | } 255 | 256 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 257 | .CodeMirror span { *vertical-align: text-bottom; } 258 | 259 | @media print { 260 | /* Hide the cursor when printing */ 261 | .CodeMirror div.CodeMirror-cursor { 262 | visibility: hidden; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /client/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | /* 11 | * Global add-ons 12 | */ 13 | 14 | .sub-header { 15 | padding-bottom: 10px; 16 | border-bottom: 1px solid #eee; 17 | } 18 | 19 | 20 | /* 21 | * Sidebar 22 | */ 23 | 24 | /* Hide for mobile, show later */ 25 | .sidebar { 26 | display: none; 27 | } 28 | @media (min-width: 768px) { 29 | .sidebar { 30 | position: fixed; 31 | top: 51px; 32 | bottom: 0; 33 | left: 0; 34 | z-index: 1000; 35 | display: block; 36 | padding: 20px; 37 | overflow-x: hidden; 38 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 39 | background-color: #EAEFF2; 40 | border-right: 1px solid #eee; 41 | } 42 | } 43 | 44 | /* Sidebar navigation */ 45 | .nav-sidebar { 46 | margin-right: -21px; /* 20px padding + 1px border */ 47 | margin-bottom: 20px; 48 | margin-left: -20px; 49 | } 50 | .nav-sidebar > li > a { 51 | padding-right: 20px; 52 | padding-left: 20px; 53 | } 54 | .nav-sidebar > .active > a { 55 | color: #fff; 56 | background-color: #00539F; 57 | } 58 | 59 | 60 | /* 61 | * Main content 62 | */ 63 | 64 | .main { 65 | padding: 20px; 66 | } 67 | @media (min-width: 768px) { 68 | .main { 69 | padding-right: 40px; 70 | padding-left: 40px; 71 | } 72 | } 73 | .main .page-header { 74 | margin-top: 0; 75 | } 76 | 77 | 78 | /* 79 | * Placeholder dashboard ideas 80 | */ 81 | 82 | .placeholders { 83 | margin-bottom: 30px; 84 | text-align: center; 85 | } 86 | .placeholders h4 { 87 | margin-bottom: 0; 88 | } 89 | .placeholder { 90 | margin-bottom: 20px; 91 | } 92 | .placeholder img { 93 | display: inline-block; 94 | border-radius: 50%; 95 | } 96 | -------------------------------------------------------------------------------- /client/css/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} -------------------------------------------------------------------------------- /client/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /client/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauljt/scanjs/b72c991c180047c4d247a29bec686f1bbebb6e88/client/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ScanJS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 45 | 46 | 47 |
48 | 49 | 85 | 86 |
87 | 88 |
89 |
90 |

Choose files to scan

91 |

Select a list of .js files, or a zip file containing .js files.

92 |

93 | 94 | 118 |
119 |
120 | 121 | 167 | 168 |
169 |
170 |

File Viewer

171 |
172 |
173 | 174 |
175 | 176 | 177 | 213 | 214 | 215 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /client/js/experimentctrl.js: -------------------------------------------------------------------------------- 1 | function ExperimentCtrl($scope,ScanSvc) { 2 | $scope.codeMirror = undefined; 3 | $scope.results=[]; 4 | $scope.ready=false; 5 | $scope.rule="eval()" 6 | 7 | var ruleData={ 8 | "name": "manual rule", 9 | "source": $scope.rule, 10 | "testhit": $scope.rule, 11 | "testmiss": "", 12 | "desc": "Manual input.", 13 | "threat": "example" 14 | } 15 | 16 | $scope.runScan = function () { 17 | $scope.results=[]; 18 | code = $scope.codeMirror.getValue(); 19 | ScanJS.loadRules(ScanSvc.rules); 20 | $scope.results=ScanJS.scan(code); 21 | $scope.lastScan=$scope.runScan; 22 | } 23 | 24 | 25 | $scope.runManualScan = function () { 26 | ruleData.source=$scope.rule; 27 | ScanJS.loadRules([ruleData]); 28 | 29 | $scope.results=[]; 30 | code = $scope.codeMirror.getValue(); 31 | //put ast on global variable for debugging purposes. 32 | try{ 33 | window.ast=acorn.parse(code); 34 | }catch(e){ 35 | 36 | } 37 | //ScanJS.setResultCallback(found); 38 | $scope.results=ScanJS.scan(code); 39 | $scope.lastScan=$scope.runManualScan; 40 | } 41 | 42 | $scope.showResult = function (filename,line, col) { 43 | document.querySelector("#code-mirror-wrapper").classList.toggle("hidden",false); 44 | $scope.codeMirror.setCursor(line - 1, col || 0); 45 | $scope.codeMirror.focus(); 46 | }; 47 | 48 | $scope.lastScan=$scope.runScan; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /client/js/lib/acorn_loose.js: -------------------------------------------------------------------------------- 1 | // Acorn: Loose parser 2 | // 3 | // This module provides an alternative parser (`parse_dammit`) that 4 | // exposes that same interface as `parse`, but will try to parse 5 | // anything as JavaScript, repairing syntax error the best it can. 6 | // There are circumstances in which it will raise an error and give 7 | // up, but they are very rare. The resulting AST will be a mostly 8 | // valid JavaScript AST (as per the [Mozilla parser API][api], except 9 | // that: 10 | // 11 | // - Return outside functions is allowed 12 | // 13 | // - Label consistency (no conflicts, break only to existing labels) 14 | // is not enforced. 15 | // 16 | // - Bogus Identifier nodes with a name of `"✖"` are inserted whenever 17 | // the parser got too confused to return anything meaningful. 18 | // 19 | // [api]: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API 20 | // 21 | // The expected use for this is to *first* try `acorn.parse`, and only 22 | // if that fails switch to `parse_dammit`. The loose parser might 23 | // parse badly indented code incorrectly, so **don't** use it as 24 | // your default parser. 25 | // 26 | // Quite a lot of acorn.js is duplicated here. The alternative was to 27 | // add a *lot* of extra cruft to that file, making it less readable 28 | // and slower. Copying and editing the code allowed me to make 29 | // invasive changes and simplifications without creating a complicated 30 | // tangle. 31 | 32 | (function(mod) { 33 | if (typeof exports == "object" && typeof module == "object") return mod(exports, require("./acorn")); // CommonJS 34 | if (typeof define == "function" && define.amd) return define(["exports", "./acorn"], mod); // AMD 35 | mod(this.acorn || (this.acorn = {}), this.acorn); // Plain browser env 36 | })(function(exports, acorn) { 37 | "use strict"; 38 | 39 | var tt = acorn.tokTypes; 40 | 41 | var options, input, fetchToken, context; 42 | 43 | exports.parse_dammit = function(inpt, opts) { 44 | if (!opts) opts = {}; 45 | input = String(inpt); 46 | options = opts; 47 | if (!opts.tabSize) opts.tabSize = 4; 48 | fetchToken = acorn.tokenize(inpt, opts); 49 | sourceFile = options.sourceFile || null; 50 | context = []; 51 | nextLineStart = 0; 52 | ahead.length = 0; 53 | next(); 54 | return parseTopLevel(); 55 | }; 56 | 57 | var lastEnd, token = {start: 0, end: 0}, ahead = []; 58 | var curLineStart, nextLineStart, curIndent, lastEndLoc, sourceFile; 59 | 60 | function next() { 61 | lastEnd = token.end; 62 | if (options.locations) 63 | lastEndLoc = token.endLoc; 64 | 65 | if (ahead.length) 66 | token = ahead.shift(); 67 | else 68 | token = readToken(); 69 | 70 | if (token.start >= nextLineStart) { 71 | while (token.start >= nextLineStart) { 72 | curLineStart = nextLineStart; 73 | nextLineStart = lineEnd(curLineStart) + 1; 74 | } 75 | curIndent = indentationAfter(curLineStart); 76 | } 77 | } 78 | 79 | function readToken() { 80 | for (;;) { 81 | try { 82 | return fetchToken(); 83 | } catch(e) { 84 | if (!(e instanceof SyntaxError)) throw e; 85 | 86 | // Try to skip some text, based on the error message, and then continue 87 | var msg = e.message, pos = e.raisedAt, replace = true; 88 | if (/unterminated/i.test(msg)) { 89 | pos = lineEnd(e.pos); 90 | if (/string/.test(msg)) { 91 | replace = {start: e.pos, end: pos, type: tt.string, value: input.slice(e.pos + 1, pos)}; 92 | } else if (/regular expr/i.test(msg)) { 93 | var re = input.slice(e.pos, pos); 94 | try { re = new RegExp(re); } catch(e) {} 95 | replace = {start: e.pos, end: pos, type: tt.regexp, value: re}; 96 | } else { 97 | replace = false; 98 | } 99 | } else if (/invalid (unicode|regexp|number)|expecting unicode|octal literal|is reserved|directly after number/i.test(msg)) { 100 | while (pos < input.length && !isSpace(input.charCodeAt(pos))) ++pos; 101 | } else if (/character escape|expected hexadecimal/i.test(msg)) { 102 | while (pos < input.length) { 103 | var ch = input.charCodeAt(pos++); 104 | if (ch === 34 || ch === 39 || isNewline(ch)) break; 105 | } 106 | } else if (/unexpected character/i.test(msg)) { 107 | pos++; 108 | replace = false; 109 | } else { 110 | throw e; 111 | } 112 | resetTo(pos); 113 | if (replace === true) replace = {start: pos, end: pos, type: tt.name, value: "✖"}; 114 | if (replace) { 115 | if (options.locations) { 116 | replace.startLoc = acorn.getLineInfo(input, replace.start); 117 | replace.endLoc = acorn.getLineInfo(input, replace.end); 118 | } 119 | return replace; 120 | } 121 | } 122 | } 123 | } 124 | 125 | function resetTo(pos) { 126 | var ch = input.charAt(pos - 1); 127 | var reAllowed = !ch || /[\[\{\(,;:?\/*=+\-~!|&%^<>]/.test(ch) || 128 | /[enwfd]/.test(ch) && /\b(keywords|case|else|return|throw|new|in|(instance|type)of|delete|void)$/.test(input.slice(pos - 10, pos)); 129 | fetchToken.jumpTo(pos, reAllowed); 130 | } 131 | 132 | function copyToken(token) { 133 | var copy = {start: token.start, end: token.end, type: token.type, value: token.value}; 134 | if (options.locations) { 135 | copy.startLoc = token.startLoc; 136 | copy.endLoc = token.endLoc; 137 | } 138 | return copy; 139 | } 140 | 141 | function lookAhead(n) { 142 | // Copy token objects, because fetchToken will overwrite the one 143 | // it returns, and in this case we still need it 144 | if (!ahead.length) 145 | token = copyToken(token); 146 | while (n > ahead.length) 147 | ahead.push(copyToken(readToken())); 148 | return ahead[n-1]; 149 | } 150 | 151 | var newline = /[\n\r\u2028\u2029]/; 152 | 153 | function isNewline(ch) { 154 | return ch === 10 || ch === 13 || ch === 8232 || ch === 8329; 155 | } 156 | function isSpace(ch) { 157 | return (ch < 14 && ch > 8) || ch === 32 || ch === 160 || isNewline(ch); 158 | } 159 | 160 | function pushCx() { 161 | context.push(curIndent); 162 | } 163 | function popCx() { 164 | curIndent = context.pop(); 165 | } 166 | 167 | function lineEnd(pos) { 168 | while (pos < input.length && !isNewline(input.charCodeAt(pos))) ++pos; 169 | return pos; 170 | } 171 | function lineStart(pos) { 172 | while (pos > 0 && !isNewline(input.charCodeAt(pos - 1))) --pos; 173 | return pos; 174 | } 175 | function indentationAfter(pos) { 176 | for (var count = 0;; ++pos) { 177 | var ch = input.charCodeAt(pos); 178 | if (ch === 32) ++count; 179 | else if (ch === 9) count += options.tabSize; 180 | else return count; 181 | } 182 | } 183 | 184 | function closes(closeTok, indent, line, blockHeuristic) { 185 | if (token.type === closeTok || token.type === tt.eof) return true; 186 | if (line != curLineStart && curIndent < indent && tokenStartsLine() && 187 | (!blockHeuristic || nextLineStart >= input.length || 188 | indentationAfter(nextLineStart) < indent)) return true; 189 | return false; 190 | } 191 | 192 | function tokenStartsLine() { 193 | for (var p = token.start - 1; p >= curLineStart; --p) { 194 | var ch = input.charCodeAt(p); 195 | if (ch !== 9 && ch !== 32) return false; 196 | } 197 | return true; 198 | } 199 | 200 | function node_t(start) { 201 | this.type = null; 202 | this.start = start; 203 | this.end = null; 204 | } 205 | 206 | function node_loc_t(start) { 207 | this.start = start || token.startLoc || {line: 1, column: 0}; 208 | this.end = null; 209 | if (sourceFile !== null) this.source = sourceFile; 210 | } 211 | 212 | function startNode() { 213 | var node = new node_t(token.start); 214 | if (options.locations) 215 | node.loc = new node_loc_t(); 216 | return node; 217 | } 218 | 219 | function startNodeFrom(other) { 220 | var node = new node_t(other.start); 221 | if (options.locations) 222 | node.loc = new node_loc_t(other.loc.start); 223 | return node; 224 | } 225 | 226 | function finishNode(node, type) { 227 | node.type = type; 228 | node.end = lastEnd; 229 | if (options.locations) 230 | node.loc.end = lastEndLoc; 231 | return node; 232 | } 233 | 234 | function getDummyLoc() { 235 | if (options.locations) { 236 | var loc = new node_loc_t(); 237 | loc.end = loc.start; 238 | return loc; 239 | } 240 | }; 241 | 242 | function dummyIdent() { 243 | var dummy = new node_t(token.start); 244 | dummy.type = "Identifier"; 245 | dummy.end = token.start; 246 | dummy.name = "✖"; 247 | dummy.loc = getDummyLoc(); 248 | return dummy; 249 | } 250 | function isDummy(node) { return node.name == "✖"; } 251 | 252 | function eat(type) { 253 | if (token.type === type) { 254 | next(); 255 | return true; 256 | } 257 | } 258 | 259 | function canInsertSemicolon() { 260 | return (token.type === tt.eof || token.type === tt.braceR || newline.test(input.slice(lastEnd, token.start))); 261 | } 262 | function semicolon() { 263 | eat(tt.semi); 264 | } 265 | 266 | function expect(type) { 267 | if (eat(type)) return true; 268 | if (lookAhead(1).type == type) { 269 | next(); next(); 270 | return true; 271 | } 272 | if (lookAhead(2).type == type) { 273 | next(); next(); next(); 274 | return true; 275 | } 276 | } 277 | 278 | function checkLVal(expr) { 279 | if (expr.type === "Identifier" || expr.type === "MemberExpression") return expr; 280 | return dummyIdent(); 281 | } 282 | 283 | function parseTopLevel() { 284 | var node = startNode(); 285 | node.body = []; 286 | while (token.type !== tt.eof) node.body.push(parseStatement()); 287 | return finishNode(node, "Program"); 288 | } 289 | 290 | function parseStatement() { 291 | var starttype = token.type, node = startNode(); 292 | 293 | switch (starttype) { 294 | case tt._break: case tt._continue: 295 | next(); 296 | var isBreak = starttype === tt._break; 297 | node.label = token.type === tt.name ? parseIdent() : null; 298 | semicolon(); 299 | return finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement"); 300 | 301 | case tt._debugger: 302 | next(); 303 | semicolon(); 304 | return finishNode(node, "DebuggerStatement"); 305 | 306 | case tt._do: 307 | next(); 308 | node.body = parseStatement(); 309 | node.test = eat(tt._while) ? parseParenExpression() : dummyIdent(); 310 | semicolon(); 311 | return finishNode(node, "DoWhileStatement"); 312 | 313 | case tt._for: 314 | next(); 315 | pushCx(); 316 | expect(tt.parenL); 317 | if (token.type === tt.semi) return parseFor(node, null); 318 | if (token.type === tt._var) { 319 | var init = startNode(); 320 | next(); 321 | parseVar(init, true); 322 | if (init.declarations.length === 1 && eat(tt._in)) 323 | return parseForIn(node, init); 324 | return parseFor(node, init); 325 | } 326 | var init = parseExpression(false, true); 327 | if (eat(tt._in)) {return parseForIn(node, checkLVal(init));} 328 | return parseFor(node, init); 329 | 330 | case tt._function: 331 | next(); 332 | return parseFunction(node, true); 333 | 334 | case tt._if: 335 | next(); 336 | node.test = parseParenExpression(); 337 | node.consequent = parseStatement(); 338 | node.alternate = eat(tt._else) ? parseStatement() : null; 339 | return finishNode(node, "IfStatement"); 340 | 341 | case tt._return: 342 | next(); 343 | if (eat(tt.semi) || canInsertSemicolon()) node.argument = null; 344 | else { node.argument = parseExpression(); semicolon(); } 345 | return finishNode(node, "ReturnStatement"); 346 | 347 | case tt._switch: 348 | var blockIndent = curIndent, line = curLineStart; 349 | next(); 350 | node.discriminant = parseParenExpression(); 351 | node.cases = []; 352 | pushCx(); 353 | expect(tt.braceL); 354 | 355 | for (var cur; !closes(tt.braceR, blockIndent, line, true);) { 356 | if (token.type === tt._case || token.type === tt._default) { 357 | var isCase = token.type === tt._case; 358 | if (cur) finishNode(cur, "SwitchCase"); 359 | node.cases.push(cur = startNode()); 360 | cur.consequent = []; 361 | next(); 362 | if (isCase) cur.test = parseExpression(); 363 | else cur.test = null; 364 | expect(tt.colon); 365 | } else { 366 | if (!cur) { 367 | node.cases.push(cur = startNode()); 368 | cur.consequent = []; 369 | cur.test = null; 370 | } 371 | cur.consequent.push(parseStatement()); 372 | } 373 | } 374 | if (cur) finishNode(cur, "SwitchCase"); 375 | popCx(); 376 | eat(tt.braceR); 377 | return finishNode(node, "SwitchStatement"); 378 | 379 | case tt._throw: 380 | next(); 381 | node.argument = parseExpression(); 382 | semicolon(); 383 | return finishNode(node, "ThrowStatement"); 384 | 385 | case tt._try: 386 | next(); 387 | node.block = parseBlock(); 388 | node.handler = null; 389 | if (token.type === tt._catch) { 390 | var clause = startNode(); 391 | next(); 392 | expect(tt.parenL); 393 | clause.param = parseIdent(); 394 | expect(tt.parenR); 395 | clause.guard = null; 396 | clause.body = parseBlock(); 397 | node.handler = finishNode(clause, "CatchClause"); 398 | } 399 | node.finalizer = eat(tt._finally) ? parseBlock() : null; 400 | if (!node.handler && !node.finalizer) return node.block; 401 | return finishNode(node, "TryStatement"); 402 | 403 | case tt._var: 404 | next(); 405 | node = parseVar(node); 406 | semicolon(); 407 | return node; 408 | 409 | case tt._while: 410 | next(); 411 | node.test = parseParenExpression(); 412 | node.body = parseStatement(); 413 | return finishNode(node, "WhileStatement"); 414 | 415 | case tt._with: 416 | next(); 417 | node.object = parseParenExpression(); 418 | node.body = parseStatement(); 419 | return finishNode(node, "WithStatement"); 420 | 421 | case tt.braceL: 422 | return parseBlock(); 423 | 424 | case tt.semi: 425 | next(); 426 | return finishNode(node, "EmptyStatement"); 427 | 428 | default: 429 | var maybeName = token.value, expr = parseExpression(); 430 | if (isDummy(expr)) { 431 | next(); 432 | if (token.type === tt.eof) return finishNode(node, "EmptyStatement"); 433 | return parseStatement(); 434 | } else if (starttype === tt.name && expr.type === "Identifier" && eat(tt.colon)) { 435 | node.body = parseStatement(); 436 | node.label = expr; 437 | return finishNode(node, "LabeledStatement"); 438 | } else { 439 | node.expression = expr; 440 | semicolon(); 441 | return finishNode(node, "ExpressionStatement"); 442 | } 443 | } 444 | } 445 | 446 | function parseBlock() { 447 | var node = startNode(); 448 | pushCx(); 449 | expect(tt.braceL); 450 | var blockIndent = curIndent, line = curLineStart; 451 | node.body = []; 452 | while (!closes(tt.braceR, blockIndent, line, true)) 453 | node.body.push(parseStatement()); 454 | popCx(); 455 | eat(tt.braceR); 456 | return finishNode(node, "BlockStatement"); 457 | } 458 | 459 | function parseFor(node, init) { 460 | node.init = init; 461 | node.test = node.update = null; 462 | if (eat(tt.semi) && token.type !== tt.semi) node.test = parseExpression(); 463 | if (eat(tt.semi) && token.type !== tt.parenR) node.update = parseExpression(); 464 | popCx(); 465 | expect(tt.parenR); 466 | node.body = parseStatement(); 467 | return finishNode(node, "ForStatement"); 468 | } 469 | 470 | function parseForIn(node, init) { 471 | node.left = init; 472 | node.right = parseExpression(); 473 | popCx(); 474 | expect(tt.parenR); 475 | node.body = parseStatement(); 476 | return finishNode(node, "ForInStatement"); 477 | } 478 | 479 | function parseVar(node, noIn) { 480 | node.declarations = []; 481 | node.kind = "var"; 482 | while (token.type === tt.name) { 483 | var decl = startNode(); 484 | decl.id = parseIdent(); 485 | decl.init = eat(tt.eq) ? parseExpression(true, noIn) : null; 486 | node.declarations.push(finishNode(decl, "VariableDeclarator")); 487 | if (!eat(tt.comma)) break; 488 | } 489 | return finishNode(node, "VariableDeclaration"); 490 | } 491 | 492 | function parseExpression(noComma, noIn) { 493 | var expr = parseMaybeAssign(noIn); 494 | if (!noComma && token.type === tt.comma) { 495 | var node = startNodeFrom(expr); 496 | node.expressions = [expr]; 497 | while (eat(tt.comma)) node.expressions.push(parseMaybeAssign(noIn)); 498 | return finishNode(node, "SequenceExpression"); 499 | } 500 | return expr; 501 | } 502 | 503 | function parseParenExpression() { 504 | pushCx(); 505 | expect(tt.parenL); 506 | var val = parseExpression(); 507 | popCx(); 508 | expect(tt.parenR); 509 | return val; 510 | } 511 | 512 | function parseMaybeAssign(noIn) { 513 | var left = parseMaybeConditional(noIn); 514 | if (token.type.isAssign) { 515 | var node = startNodeFrom(left); 516 | node.operator = token.value; 517 | node.left = checkLVal(left); 518 | next(); 519 | node.right = parseMaybeAssign(noIn); 520 | return finishNode(node, "AssignmentExpression"); 521 | } 522 | return left; 523 | } 524 | 525 | function parseMaybeConditional(noIn) { 526 | var expr = parseExprOps(noIn); 527 | if (eat(tt.question)) { 528 | var node = startNodeFrom(expr); 529 | node.test = expr; 530 | node.consequent = parseExpression(true); 531 | node.alternate = expect(tt.colon) ? parseExpression(true, noIn) : dummyIdent(); 532 | return finishNode(node, "ConditionalExpression"); 533 | } 534 | return expr; 535 | } 536 | 537 | function parseExprOps(noIn) { 538 | var indent = curIndent, line = curLineStart; 539 | return parseExprOp(parseMaybeUnary(noIn), -1, noIn, indent, line); 540 | } 541 | 542 | function parseExprOp(left, minPrec, noIn, indent, line) { 543 | if (curLineStart != line && curIndent < indent && tokenStartsLine()) return left; 544 | var prec = token.type.binop; 545 | if (prec != null && (!noIn || token.type !== tt._in)) { 546 | if (prec > minPrec) { 547 | var node = startNodeFrom(left); 548 | node.left = left; 549 | node.operator = token.value; 550 | next(); 551 | if (curLineStart != line && curIndent < indent && tokenStartsLine()) 552 | node.right = dummyIdent(); 553 | else 554 | node.right = parseExprOp(parseMaybeUnary(noIn), prec, noIn, indent, line); 555 | var node = finishNode(node, /&&|\|\|/.test(node.operator) ? "LogicalExpression" : "BinaryExpression"); 556 | return parseExprOp(node, minPrec, noIn, indent, line); 557 | } 558 | } 559 | return left; 560 | } 561 | 562 | function parseMaybeUnary(noIn) { 563 | if (token.type.prefix) { 564 | var node = startNode(), update = token.type.isUpdate; 565 | node.operator = token.value; 566 | node.prefix = true; 567 | next(); 568 | node.argument = parseMaybeUnary(noIn); 569 | if (update) node.argument = checkLVal(node.argument); 570 | return finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); 571 | } 572 | var expr = parseExprSubscripts(); 573 | while (token.type.postfix && !canInsertSemicolon()) { 574 | var node = startNodeFrom(expr); 575 | node.operator = token.value; 576 | node.prefix = false; 577 | node.argument = checkLVal(expr); 578 | next(); 579 | expr = finishNode(node, "UpdateExpression"); 580 | } 581 | return expr; 582 | } 583 | 584 | function parseExprSubscripts() { 585 | var indent = curIndent, line = curLineStart; 586 | return parseSubscripts(parseExprAtom(), false, curIndent, line); 587 | } 588 | 589 | function parseSubscripts(base, noCalls, startIndent, line) { 590 | for (;;) { 591 | if (curLineStart != line && curIndent <= startIndent && tokenStartsLine()) { 592 | if (token.type == tt.dot && curIndent == startIndent) 593 | --startIndent; 594 | else 595 | return base; 596 | } 597 | 598 | if (eat(tt.dot)) { 599 | var node = startNodeFrom(base); 600 | node.object = base; 601 | if (curLineStart != line && curIndent <= startIndent && tokenStartsLine()) 602 | node.property = dummyIdent(); 603 | else 604 | node.property = parsePropertyName() || dummyIdent(); 605 | node.computed = false; 606 | base = finishNode(node, "MemberExpression"); 607 | } else if (token.type == tt.bracketL) { 608 | pushCx(); 609 | next(); 610 | var node = startNodeFrom(base); 611 | node.object = base; 612 | node.property = parseExpression(); 613 | node.computed = true; 614 | popCx(); 615 | expect(tt.bracketR); 616 | base = finishNode(node, "MemberExpression"); 617 | } else if (!noCalls && token.type == tt.parenL) { 618 | pushCx(); 619 | var node = startNodeFrom(base); 620 | node.callee = base; 621 | node.arguments = parseExprList(tt.parenR); 622 | base = finishNode(node, "CallExpression"); 623 | } else { 624 | return base; 625 | } 626 | } 627 | } 628 | 629 | function parseExprAtom() { 630 | switch (token.type) { 631 | case tt._this: 632 | var node = startNode(); 633 | next(); 634 | return finishNode(node, "ThisExpression"); 635 | case tt.name: 636 | return parseIdent(); 637 | case tt.num: case tt.string: case tt.regexp: 638 | var node = startNode(); 639 | node.value = token.value; 640 | node.raw = input.slice(token.start, token.end); 641 | next(); 642 | return finishNode(node, "Literal"); 643 | 644 | case tt._null: case tt._true: case tt._false: 645 | var node = startNode(); 646 | node.value = token.type.atomValue; 647 | node.raw = token.type.keyword; 648 | next(); 649 | return finishNode(node, "Literal"); 650 | 651 | case tt.parenL: 652 | var tokStart1 = token.start; 653 | next(); 654 | var val = parseExpression(); 655 | val.start = tokStart1; 656 | val.end = token.end; 657 | expect(tt.parenR); 658 | return val; 659 | 660 | case tt.bracketL: 661 | var node = startNode(); 662 | pushCx(); 663 | node.elements = parseExprList(tt.bracketR); 664 | return finishNode(node, "ArrayExpression"); 665 | 666 | case tt.braceL: 667 | return parseObj(); 668 | 669 | case tt._function: 670 | var node = startNode(); 671 | next(); 672 | return parseFunction(node, false); 673 | 674 | case tt._new: 675 | return parseNew(); 676 | 677 | default: 678 | return dummyIdent(); 679 | } 680 | } 681 | 682 | function parseNew() { 683 | var node = startNode(), startIndent = curIndent, line = curLineStart; 684 | next(); 685 | node.callee = parseSubscripts(parseExprAtom(), true, startIndent, line); 686 | if (token.type == tt.parenL) { 687 | pushCx(); 688 | node.arguments = parseExprList(tt.parenR); 689 | } else { 690 | node.arguments = []; 691 | } 692 | return finishNode(node, "NewExpression"); 693 | } 694 | 695 | function parseObj() { 696 | var node = startNode(); 697 | node.properties = []; 698 | pushCx(); 699 | next(); 700 | var propIndent = curIndent, line = curLineStart; 701 | while (!closes(tt.braceR, propIndent, line)) { 702 | var name = parsePropertyName(); 703 | if (!name) { if (isDummy(parseExpression(true))) next(); eat(tt.comma); continue; } 704 | var prop = {key: name}, isGetSet = false, kind; 705 | if (eat(tt.colon)) { 706 | prop.value = parseExpression(true); 707 | kind = prop.kind = "init"; 708 | } else if (options.ecmaVersion >= 5 && prop.key.type === "Identifier" && 709 | (prop.key.name === "get" || prop.key.name === "set")) { 710 | isGetSet = true; 711 | kind = prop.kind = prop.key.name; 712 | prop.key = parsePropertyName() || dummyIdent(); 713 | prop.value = parseFunction(startNode(), false); 714 | } else { 715 | next(); 716 | eat(tt.comma); 717 | continue; 718 | } 719 | 720 | node.properties.push(prop); 721 | eat(tt.comma); 722 | } 723 | popCx(); 724 | eat(tt.braceR); 725 | return finishNode(node, "ObjectExpression"); 726 | } 727 | 728 | function parsePropertyName() { 729 | if (token.type === tt.num || token.type === tt.string) return parseExprAtom(); 730 | if (token.type === tt.name || token.type.keyword) return parseIdent(); 731 | } 732 | 733 | function parseIdent() { 734 | var node = startNode(); 735 | node.name = token.type === tt.name ? token.value : token.type.keyword; 736 | next(); 737 | return finishNode(node, "Identifier"); 738 | } 739 | 740 | function parseFunction(node, isStatement) { 741 | if (token.type === tt.name) node.id = parseIdent(); 742 | else if (isStatement) node.id = dummyIdent(); 743 | else node.id = null; 744 | node.params = []; 745 | pushCx(); 746 | expect(tt.parenL); 747 | while (token.type == tt.name) { 748 | node.params.push(parseIdent()); 749 | eat(tt.comma); 750 | } 751 | popCx(); 752 | eat(tt.parenR); 753 | node.body = parseBlock(); 754 | return finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression"); 755 | } 756 | 757 | function parseExprList(close) { 758 | var indent = curIndent, line = curLineStart, elts = [], continuedLine = nextLineStart; 759 | next(); // Opening bracket 760 | while (!closes(close, indent + (curLineStart <= continuedLine ? 1 : 0), line)) { 761 | var elt = parseExpression(true); 762 | if (isDummy(elt)) { 763 | if (closes(close, indent, line)) break; 764 | next(); 765 | } else { 766 | elts.push(elt); 767 | } 768 | while (eat(tt.comma)) {} 769 | } 770 | popCx(); 771 | eat(close); 772 | return elts; 773 | } 774 | }); 775 | -------------------------------------------------------------------------------- /client/js/lib/localforage.min.js: -------------------------------------------------------------------------------- 1 | !function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)k.push("exports"===i[l]?g={}:b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;ja?(b&&b(null),void c(null)):void localforage.ready().then(function(){var e=l.transaction(j,"readonly").objectStore(j),f=!1,g=e.openCursor();g.onsuccess=function(){var d=g.result;return d?void(0===a?(b&&b(d.key),c(d.key)):f?(b&&b(d.key),c(d.key)):(f=!0,d.advance(a))):(b&&b(null),void c(null))},g.onerror=function(){d(g.error.name)}})})}var h="localforage",i=1,j="keyvaluepairs",k=window.Promise,l=null,m=m||window.indexedDB||window.webkitIndexedDB||window.mozIndexedDB||window.OIndexedDB||window.msIndexedDB;if(m){var n={_driver:"asyncStorage",_initStorage:a,getItem:b,setItem:c,removeItem:d,clear:e,length:f,key:g};"function"==typeof define&&define.amd?define("asyncStorage",function(){return n}):"undefined"!=typeof module&&module.exports?module.exports=n:this.asyncStorage=n}}.call(this),function(){"use strict";function a(){return h.resolve()}function b(a){return new h(function(b){localforage.ready().then(function(){i.clear(),a&&a(),b()})})}function c(a,b){return new h(function(c,d){localforage.ready().then(function(){try{var e=i.getItem(a);e&&(e=JSON.parse(e)),b&&b(e),c(e)}catch(f){d(f)}})})}function d(a,b){return new h(function(c){localforage.ready().then(function(){var d=i.key(a);b&&b(d),c(d)})})}function e(a){return new h(function(b){localforage.ready().then(function(){var c=i.length;a&&a(c),b(c)})})}function f(a,b){return new h(function(c){localforage.ready().then(function(){i.removeItem(a),b&&b(),c()})})}function g(a,b,c){return new h(function(d,e){localforage.ready().then(function(){void 0===b&&(b=null);var f=b;try{b=JSON.stringify(b)}catch(g){e(g)}i.setItem(a,b),c&&c(f),d(f)})})}var h=window.Promise,i=null;try{i=window.localStorage}catch(j){return}var k={_driver:"localStorageWrapper",_initStorage:a,getItem:c,setItem:g,removeItem:f,clear:b,length:e,key:d};"function"==typeof define&&define.amd?define("localStorageWrapper",function(){return k}):"undefined"!=typeof module&&module.exports?module.exports=k:this.localStorageWrapper=k}.call(this),function(){"use strict";function a(){return new n(function(a){o=window.openDatabase(h,j,m,i),o.transaction(function(b){b.executeSql("CREATE TABLE IF NOT EXISTS localforage (id INTEGER PRIMARY KEY, key unique, value)",[],function(){a()},null)})})}function b(a,b){return new n(function(c,d){localforage.ready().then(function(){o.transaction(function(e){e.executeSql("SELECT * FROM localforage WHERE key = ? LIMIT 1",[a],function(a,e){var f=e.rows.length?e.rows.item(0).value:null;if(f&&f.substr(0,l)===k)try{f=JSON.parse(f.slice(l))}catch(g){d(g)}b&&b(f),c(f)},null)})})})}function c(a,b,c){return new n(function(d){localforage.ready().then(function(){void 0===b&&(b=null);var e;e="boolean"==typeof b||"number"==typeof b||"object"==typeof b?k+JSON.stringify(b):b,o.transaction(function(f){f.executeSql("INSERT OR REPLACE INTO localforage (key, value) VALUES (?, ?)",[a,e],function(){c&&c(b),d(b)},null)})})})}function d(a,b){return new n(function(c){localforage.ready().then(function(){o.transaction(function(d){d.executeSql("DELETE FROM localforage WHERE key = ?",[a],function(){b&&b(),c()},null)})})})}function e(a){return new n(function(b){localforage.ready().then(function(){o.transaction(function(c){c.executeSql("DELETE FROM localforage",[],function(){a&&a(),b()},null)})})})}function f(a){return new n(function(b){localforage.ready().then(function(){o.transaction(function(c){c.executeSql("SELECT COUNT(key) as c FROM localforage",[],function(c,d){var e=d.rows.item(0).c;a&&a(e),b(e)},null)})})})}function g(a,b){return new n(function(c){localforage.ready().then(function(){o.transaction(function(d){d.executeSql("SELECT key FROM localforage WHERE id = ? LIMIT 1",[a+1],function(a,d){var e=d.rows.length?d.rows.item(0).key:null;b&&b(e),c(e)},null)})})})}var h="localforage",i=4980736,j="1.0",k="__lfsc__:",l=k.length,m="keyvaluepairs",n=window.Promise,o=null;if(window.openDatabase){var p={_driver:"webSQLStorage",_initStorage:a,getItem:b,setItem:c,removeItem:d,clear:e,length:f,key:g};"function"==typeof define&&define.amd?define("webSQLStorage",function(){return p}):"undefined"!=typeof module&&module.exports?module.exports=p:this.webSQLStorage=p}}.call(this),function(){"use strict";var a=window.Promise,b=1,c=2,d=3,e=d;"function"==typeof define&&define.amd?e=b:"undefined"!=typeof module&&module.exports&&(e=c);var f,g=g||window.indexedDB||window.webkitIndexedDB||window.mozIndexedDB||window.OIndexedDB||window.msIndexedDB,h=this,i={INDEXEDDB:"asyncStorage",LOCALSTORAGE:"localStorageWrapper",WEBSQL:"webSQLStorage",driver:function(){return i._driver||null},_ready:a.reject(new Error("setDriver() wasn't called")),setDriver:function(d,f){return this._ready=new a(function(a,j){if(!g&&d===i.INDEXEDDB||!window.openDatabase&&d===i.WEBSQL)return f&&f(i),void j(i);if(e===b)require([d],function(b){i._extend(b),i._initStorage().then(function(){f&&f(i),a(i)})});else if(e===c){var k;switch(d){case i.INDEXEDDB:k=require("localforage/src/drivers/indexeddb");break;case i.LOCALSTORAGE:k=require("localforage/src/drivers/localstorage");break;case i.WEBSQL:k=require("localforage/src/drivers/websql")}i._extend(k),i._initStorage().then(function(){f&&f(i),a(i)})}else i._extend(h[d]),i._initStorage().then(function(){f&&f(i),a(i)})}),i._ready},ready:function(){return this._ready},_extend:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b])}};f=g?i.INDEXEDDB:window.openDatabase?i.WEBSQL:i.LOCALSTORAGE,i.setDriver(f),e===b?define(function(){return i}):e===c?module.exports=i:this.localforage=i}.call(this); -------------------------------------------------------------------------------- /client/js/lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | //Array.find polyfill 3 | if (!Array.prototype.find) { 4 | Object.defineProperty(Array.prototype, 'find', { 5 | enumerable: false, 6 | configurable: false, 7 | writable: false, 8 | value: function(predicate) { 9 | for (var i=0; i < this.length; i++) { 10 | if (predicate(this[i], i, this)) { 11 | return this[i]; 12 | } 13 | } 14 | return void 0; 15 | } 16 | }); 17 | } -------------------------------------------------------------------------------- /client/js/lib/walk.js: -------------------------------------------------------------------------------- 1 | // AST walker module for Mozilla Parser API compatible trees 2 | 3 | (function(mod) { 4 | if (typeof exports == "object" && typeof module == "object") return mod(exports); // CommonJS 5 | if (typeof define == "function" && define.amd) return define(["exports"], mod); // AMD 6 | mod((this.acorn || (this.acorn = {})).walk = {}); // Plain browser env 7 | })(function(exports) { 8 | "use strict"; 9 | 10 | // A simple walk is one where you simply specify callbacks to be 11 | // called on specific nodes. The last two arguments are optional. A 12 | // simple use would be 13 | // 14 | // walk.simple(myTree, { 15 | // Expression: function(node) { ... } 16 | // }); 17 | // 18 | // to do something with all expressions. All Parser API node types 19 | // can be used to identify node types, as well as Expression, 20 | // Statement, and ScopeBody, which denote categories of nodes. 21 | // 22 | // The base argument can be used to pass a custom (recursive) 23 | // walker, and state can be used to give this walked an initial 24 | // state. 25 | exports.simple = function(node, visitors, base, state) { 26 | if (!base) base = exports.base; 27 | function c(node, st, override) { 28 | var type = override || node.type; 29 | var found = visitors[type]; 30 | base[type](node, st, c); 31 | if (found) found(node, st); 32 | } 33 | c(node, state); 34 | }; 35 | 36 | // An ancestor walk builds up an array of ancestor nodes (including 37 | // the current node) and passes them to the callback as the state parameter. 38 | exports.ancestor = function(node, visitors, base, state) { 39 | if (!base) base = exports.base; 40 | if (!state) state = []; 41 | function c(node, st, override) { 42 | var type = override || node.type, found = visitors[type]; 43 | if (node != st[st.length - 1]) { 44 | st = st.slice(); 45 | st.push(node); 46 | } 47 | base[type](node, st, c); 48 | if (found) found(node, st); 49 | } 50 | c(node, state); 51 | }; 52 | 53 | // A recursive walk is one where your functions override the default 54 | // walkers. They can modify and replace the state parameter that's 55 | // threaded through the walk, and can opt how and whether to walk 56 | // their child nodes (by calling their third argument on these 57 | // nodes). 58 | exports.recursive = function(node, state, funcs, base) { 59 | var visitor = funcs ? exports.make(funcs, base) : base; 60 | function c(node, st, override) { 61 | visitor[override || node.type](node, st, c); 62 | } 63 | c(node, state); 64 | }; 65 | 66 | function makeTest(test) { 67 | if (typeof test == "string") 68 | return function(type) { return type == test; }; 69 | else if (!test) 70 | return function() { return true; }; 71 | else 72 | return test; 73 | } 74 | 75 | function Found(node, state) { this.node = node; this.state = state; } 76 | 77 | // Find a node with a given start, end, and type (all are optional, 78 | // null can be used as wildcard). Returns a {node, state} object, or 79 | // undefined when it doesn't find a matching node. 80 | exports.findNodeAt = function(node, start, end, test, base, state) { 81 | test = makeTest(test); 82 | try { 83 | if (!base) base = exports.base; 84 | var c = function(node, st, override) { 85 | var type = override || node.type; 86 | if ((start == null || node.start <= start) && 87 | (end == null || node.end >= end)) 88 | base[type](node, st, c); 89 | if (test(type, node) && 90 | (start == null || node.start == start) && 91 | (end == null || node.end == end)) 92 | throw new Found(node, st); 93 | }; 94 | c(node, state); 95 | } catch (e) { 96 | if (e instanceof Found) return e; 97 | throw e; 98 | } 99 | }; 100 | 101 | // Find the innermost node of a given type that contains the given 102 | // position. Interface similar to findNodeAt. 103 | exports.findNodeAround = function(node, pos, test, base, state) { 104 | test = makeTest(test); 105 | try { 106 | if (!base) base = exports.base; 107 | var c = function(node, st, override) { 108 | var type = override || node.type; 109 | if (node.start > pos || node.end < pos) return; 110 | base[type](node, st, c); 111 | if (test(type, node)) throw new Found(node, st); 112 | }; 113 | c(node, state); 114 | } catch (e) { 115 | if (e instanceof Found) return e; 116 | throw e; 117 | } 118 | }; 119 | 120 | // Find the outermost matching node after a given position. 121 | exports.findNodeAfter = function(node, pos, test, base, state) { 122 | test = makeTest(test); 123 | try { 124 | if (!base) base = exports.base; 125 | var c = function(node, st, override) { 126 | if (node.end < pos) return; 127 | var type = override || node.type; 128 | if (node.start >= pos && test(type, node)) throw new Found(node, st); 129 | base[type](node, st, c); 130 | }; 131 | c(node, state); 132 | } catch (e) { 133 | if (e instanceof Found) return e; 134 | throw e; 135 | } 136 | }; 137 | 138 | // Find the outermost matching node before a given position. 139 | exports.findNodeBefore = function(node, pos, test, base, state) { 140 | test = makeTest(test); 141 | if (!base) base = exports.base; 142 | var max; 143 | var c = function(node, st, override) { 144 | if (node.start > pos) return; 145 | var type = override || node.type; 146 | if (node.end <= pos && (!max || max.node.end < node.end) && test(type, node)) 147 | max = new Found(node, st); 148 | base[type](node, st, c); 149 | }; 150 | c(node, state); 151 | return max; 152 | }; 153 | 154 | // Used to create a custom walker. Will fill in all missing node 155 | // type properties with the defaults. 156 | exports.make = function(funcs, base) { 157 | if (!base) base = exports.base; 158 | var visitor = {}; 159 | for (var type in base) visitor[type] = base[type]; 160 | for (var type in funcs) visitor[type] = funcs[type]; 161 | return visitor; 162 | }; 163 | 164 | function skipThrough(node, st, c) { c(node, st); } 165 | function ignore(_node, _st, _c) {} 166 | 167 | // Node walkers. 168 | 169 | var base = exports.base = {}; 170 | base.Program = base.BlockStatement = function(node, st, c) { 171 | for (var i = 0; i < node.body.length; ++i) 172 | c(node.body[i], st, "Statement"); 173 | }; 174 | base.Statement = skipThrough; 175 | base.EmptyStatement = ignore; 176 | base.ExpressionStatement = function(node, st, c) { 177 | c(node.expression, st, "Expression"); 178 | }; 179 | base.IfStatement = function(node, st, c) { 180 | c(node.test, st, "Expression"); 181 | c(node.consequent, st, "Statement"); 182 | if (node.alternate) c(node.alternate, st, "Statement"); 183 | }; 184 | base.LabeledStatement = function(node, st, c) { 185 | c(node.body, st, "Statement"); 186 | }; 187 | base.BreakStatement = base.ContinueStatement = ignore; 188 | base.WithStatement = function(node, st, c) { 189 | c(node.object, st, "Expression"); 190 | c(node.body, st, "Statement"); 191 | }; 192 | base.SwitchStatement = function(node, st, c) { 193 | c(node.discriminant, st, "Expression"); 194 | for (var i = 0; i < node.cases.length; ++i) { 195 | var cs = node.cases[i]; 196 | if (cs.test) c(cs.test, st, "Expression"); 197 | for (var j = 0; j < cs.consequent.length; ++j) 198 | c(cs.consequent[j], st, "Statement"); 199 | } 200 | }; 201 | base.ReturnStatement = function(node, st, c) { 202 | if (node.argument) c(node.argument, st, "Expression"); 203 | }; 204 | base.ThrowStatement = function(node, st, c) { 205 | c(node.argument, st, "Expression"); 206 | }; 207 | base.TryStatement = function(node, st, c) { 208 | c(node.block, st, "Statement"); 209 | if (node.handler) c(node.handler.body, st, "ScopeBody"); 210 | if (node.finalizer) c(node.finalizer, st, "Statement"); 211 | }; 212 | base.WhileStatement = function(node, st, c) { 213 | c(node.test, st, "Expression"); 214 | c(node.body, st, "Statement"); 215 | }; 216 | base.DoWhileStatement = base.WhileStatement; 217 | base.ForStatement = function(node, st, c) { 218 | if (node.init) c(node.init, st, "ForInit"); 219 | if (node.test) c(node.test, st, "Expression"); 220 | if (node.update) c(node.update, st, "Expression"); 221 | c(node.body, st, "Statement"); 222 | }; 223 | base.ForInStatement = function(node, st, c) { 224 | c(node.left, st, "ForInit"); 225 | c(node.right, st, "Expression"); 226 | c(node.body, st, "Statement"); 227 | }; 228 | base.ForInit = function(node, st, c) { 229 | if (node.type == "VariableDeclaration") c(node, st); 230 | else c(node, st, "Expression"); 231 | }; 232 | base.DebuggerStatement = ignore; 233 | 234 | base.FunctionDeclaration = function(node, st, c) { 235 | c(node, st, "Function"); 236 | }; 237 | base.VariableDeclaration = function(node, st, c) { 238 | for (var i = 0; i < node.declarations.length; ++i) { 239 | var decl = node.declarations[i]; 240 | if (decl.init) c(decl.init, st, "Expression"); 241 | } 242 | }; 243 | 244 | base.Function = function(node, st, c) { 245 | c(node.body, st, "ScopeBody"); 246 | }; 247 | base.ScopeBody = function(node, st, c) { 248 | c(node, st, "Statement"); 249 | }; 250 | 251 | base.Expression = skipThrough; 252 | base.ThisExpression = ignore; 253 | base.ArrayExpression = function(node, st, c) { 254 | for (var i = 0; i < node.elements.length; ++i) { 255 | var elt = node.elements[i]; 256 | if (elt) c(elt, st, "Expression"); 257 | } 258 | }; 259 | base.ObjectExpression = function(node, st, c) { 260 | for (var i = 0; i < node.properties.length; ++i) 261 | c(node.properties[i].value, st, "Expression"); 262 | }; 263 | base.FunctionExpression = base.FunctionDeclaration; 264 | base.SequenceExpression = function(node, st, c) { 265 | for (var i = 0; i < node.expressions.length; ++i) 266 | c(node.expressions[i], st, "Expression"); 267 | }; 268 | base.UnaryExpression = base.UpdateExpression = function(node, st, c) { 269 | c(node.argument, st, "Expression"); 270 | }; 271 | base.BinaryExpression = base.AssignmentExpression = base.LogicalExpression = function(node, st, c) { 272 | c(node.left, st, "Expression"); 273 | c(node.right, st, "Expression"); 274 | }; 275 | base.ConditionalExpression = function(node, st, c) { 276 | c(node.test, st, "Expression"); 277 | c(node.consequent, st, "Expression"); 278 | c(node.alternate, st, "Expression"); 279 | }; 280 | base.NewExpression = base.CallExpression = function(node, st, c) { 281 | c(node.callee, st, "Expression"); 282 | if (node.arguments) for (var i = 0; i < node.arguments.length; ++i) 283 | c(node.arguments[i], st, "Expression"); 284 | }; 285 | base.MemberExpression = function(node, st, c) { 286 | c(node.object, st, "Expression"); 287 | if (node.computed) c(node.property, st, "Expression"); 288 | }; 289 | base.Identifier = base.Literal = ignore; 290 | 291 | // A custom walker that keeps track of the scope chain and the 292 | // variables defined in it. 293 | function makeScope(prev, isCatch) { 294 | return {vars: Object.create(null), prev: prev, isCatch: isCatch}; 295 | } 296 | function normalScope(scope) { 297 | while (scope.isCatch) scope = scope.prev; 298 | return scope; 299 | } 300 | exports.scopeVisitor = exports.make({ 301 | Function: function(node, scope, c) { 302 | var inner = makeScope(scope); 303 | for (var i = 0; i < node.params.length; ++i) 304 | inner.vars[node.params[i].name] = {type: "argument", node: node.params[i]}; 305 | if (node.id) { 306 | var decl = node.type == "FunctionDeclaration"; 307 | (decl ? normalScope(scope) : inner).vars[node.id.name] = 308 | {type: decl ? "function" : "function name", node: node.id}; 309 | } 310 | c(node.body, inner, "ScopeBody"); 311 | }, 312 | TryStatement: function(node, scope, c) { 313 | c(node.block, scope, "Statement"); 314 | if (node.handler) { 315 | var inner = makeScope(scope, true); 316 | inner.vars[node.handler.param.name] = {type: "catch clause", node: node.handler.param}; 317 | c(node.handler.body, inner, "ScopeBody"); 318 | } 319 | if (node.finalizer) c(node.finalizer, scope, "Statement"); 320 | }, 321 | VariableDeclaration: function(node, scope, c) { 322 | var target = normalScope(scope); 323 | for (var i = 0; i < node.declarations.length; ++i) { 324 | var decl = node.declarations[i]; 325 | target.vars[decl.id.name] = {type: "var", node: decl.id}; 326 | if (decl.init) c(decl.init, scope, "Expression"); 327 | } 328 | } 329 | }); 330 | 331 | }); -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | var scanjsModule = angular.module('scanjs', ['ui.bootstrap']); 2 | 3 | angular.element(document).ready(function() { 4 | //makes navbar shows/hides elements with the 'panel' class 5 | var panels=document.getElementsByClassName("panel"); 6 | document.querySelector("#header-links").addEventListener("click", function(evt){ 7 | var active=evt.target.id.split('-')[0]+"-wrapper"; 8 | Array.prototype.forEach.call(panels,function(panel){ 9 | panel.classList.toggle("hidden",panel.id!=active); 10 | }); 11 | if(active === 'experiment-wrapper'){ 12 | //fixes bug where codemirror is blank until you click it 13 | var experimentCtrlScope = angular.element(document.getElementById("experiment-wrapper")).scope(); 14 | experimentCtrlScope.codeMirror.refresh(); 15 | } 16 | }); 17 | 18 | //Hack since angular doesn't support file inputs yet :( 19 | document.getElementById("scan-file-input").addEventListener("change", function(evt) { 20 | angular.element(evt.target).scope().handleFileUpload(this.files); 21 | }); 22 | 23 | document.getElementById("rule-file-input").addEventListener("change", function(evt) { 24 | angular.element(evt.target).scope().handleFileUpload(this.files); 25 | }); 26 | 27 | 28 | // loading codeMirror requires the textArea 29 | var scanCtrlScope = angular.element(document.getElementById("scan-wrapper")).scope(); 30 | scanCtrlScope.codeMirror = new CodeMirror(document.getElementById('codeMirrorDiv'), { 31 | mode: 'javascript', 32 | lineNumbers: true, 33 | theme: 'mdn-like', 34 | value: "", 35 | readOnly:true, 36 | tabsize: 2, 37 | styleActiveLine: true 38 | }); 39 | 40 | var experimentCtrlScope = angular.element(document.getElementById("experiment-wrapper")).scope(); 41 | experimentCtrlScope.codeMirror = new CodeMirror(document.getElementById('experiment-mirror'), { 42 | mode: 'javascript', 43 | lineNumbers: true, 44 | theme: 'mdn-like', 45 | value: "bar.foo\nfoo=something\nbaz.bar=stuff\nfoo(something)\nfoo.bar\nfoo.bar()\neval(test)\nfoo.innerHTML=danger", 46 | tabsize: 2, 47 | styleActiveLine: true 48 | }); 49 | 50 | //prettify stuffs 51 | prettyPrint(); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /client/js/prettify.js: -------------------------------------------------------------------------------- 1 | !function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a= 3 | b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;ah[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com", 11 | /^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+ 12 | s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, 13 | q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= 14 | c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, 21 | V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", 22 | /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);p(C([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/], 23 | ["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);p(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);p(v({keywords:M,hashComments:!0,cStyleComments:!0,types:R}),["c","cc","cpp","cxx","cyc","m"]);p(v({keywords:"null,true,false"}),["json"]);p(v({keywords:O,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:R}), 24 | ["cs"]);p(v({keywords:N,cStyleComments:!0}),["java"]);p(v({keywords:y,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);p(v({keywords:P,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);p(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);p(v({keywords:Q, 25 | hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);p(v({keywords:E,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);p(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);p(v({keywords:W,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]); 26 | p(C([],[["str",/^[\S\s]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:C,registerLangHandler:p,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,g){var b=document.createElement("div");b.innerHTML="
"+a+"
";b=b.firstChild;g&&J(b,g,!0);K({h:d,j:g,c:b,i:1}); 27 | return b.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function g(){for(var b=D.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;i= 200 && request.status < 400) { 14 | $scope.rules = JSON.parse(request.responseText); 15 | 16 | ScanSvc.loadRules($scope.rules) 17 | } else { 18 | console.log('Error loading ' + rules) 19 | } 20 | }; 21 | 22 | request.onerror = function () { 23 | console.log('Connection error while loading ' + rulesFile) 24 | }; 25 | request.send(); 26 | } 27 | 28 | $scope.handleFileUpload = function handleFileUpload(fileList) { 29 | 30 | $scope.rulesFile = fileList[0].name; 31 | 32 | var reader = new FileReader(); 33 | reader.onload = function () { 34 | $scope.rules = JSON.parse(this.result); 35 | ScanSvc.loadRules($scope.rules); 36 | $scope.$apply(); 37 | } 38 | 39 | reader.readAsText(fileList[0]) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/js/scanctrl.js: -------------------------------------------------------------------------------- 1 | function ScanCtrl($scope, ScanSvc) { 2 | $scope.codeMirror = undefined; 3 | $scope.codeMirrorManual = undefined; 4 | $scope.inputFiles = []; 5 | $scope.results=[]; 6 | $scope.errors=[]; 7 | $scope.filteredResults=[]; 8 | $scope.inputFilename=""; 9 | $scope.issueList=[]; 10 | $scope.throbInput = false; 11 | $scope.throbOutput = false; 12 | 13 | var pending = 0; 14 | var selectedFile = 0; 15 | var codeMirror_index = 0; 16 | 17 | $scope.run = function (source, filename) { 18 | //empty last scan 19 | $scope.results=[]; 20 | $scope.errors=[]; 21 | $scope.inputFiles.forEach(function (scriptFile, i) { 22 | if (document.getElementById('doScan_'+i).checked) { 23 | pending++; 24 | $scope.throbOutput = true; 25 | ScanSvc.newScan(scriptFile.name,scriptFile.asText()); 26 | } 27 | }); 28 | 29 | //update UI 30 | document.querySelector("#scan-input").classList.toggle("hidden",true); 31 | document.querySelector("#scan-results").classList.toggle("hidden",false); 32 | document.querySelector("#scan-output-rules").classList.toggle("hidden", false); 33 | document.querySelector("#scan-output-files").classList.toggle("hidden", false); 34 | 35 | //update navbar 36 | document.querySelector("#scan-input-nav").classList.toggle("active",false); 37 | document.querySelector("#scan-output-nav").classList.toggle("active",true); 38 | } 39 | 40 | $scope.updateIssueList = function(){ 41 | $scope.issueList = $scope.results.reduce(function(p, c) { 42 | if ((c.type == 'finding') && (typeof p !== "undefined")) { 43 | if (p.indexOf(c.rule.name) < 0) { 44 | p.push(c.rule.name); 45 | } 46 | return p; 47 | } 48 | }, []); 49 | } 50 | 51 | $scope.filterResults=function(issue){ 52 | if(!issue){ 53 | $scope.filteredResults=$scope.results; 54 | } 55 | else{ 56 | if(typeof issue.name != "undefined") { 57 | $scope.filteredResults=$scope.results.filter(function(result){ 58 | return result.filename === issue.name; 59 | }); 60 | }else { 61 | $scope.filteredResults=$scope.results.filter(function(result){ 62 | return result.rule.name == issue; 63 | }); 64 | } 65 | } 66 | } 67 | 68 | $scope.navShowInput = function () { 69 | //show input tab, hide results 70 | document.querySelector("#scan-input").classList.toggle("hidden", false); 71 | document.querySelector("#scan-results").classList.toggle("hidden", true); 72 | document.querySelector("#scan-output-rules").classList.toggle("hidden", true); 73 | document.querySelector("#scan-output-files").classList.toggle("hidden", true); 74 | //make input the active nav element 75 | document.querySelector("#scan-input-nav").classList.toggle("active", true); 76 | document.querySelector("#scan-output-nav").classList.toggle("active", false); 77 | } 78 | 79 | $scope.navShowOutput = function (filterIssue) { 80 | //show input tab, hide results 81 | document.querySelector("#scan-input").classList.toggle("hidden", true); 82 | document.querySelector("#scan-results").classList.toggle("hidden", false); 83 | document.querySelector("#scan-output-rules").classList.toggle("hidden", false); 84 | document.querySelector("#scan-output-files").classList.toggle("hidden", false); 85 | //make input the active nav element 86 | document.querySelector("#scan-input-nav").classList.toggle("active", false); 87 | document.querySelector("#scan-output-nav").classList.toggle("active", true); 88 | $scope.filterResults(filterIssue); 89 | } 90 | 91 | $scope.handleFileUpload = function handleFileUpload(fileList) { 92 | function handleMaybeZip() { 93 | //packaged app case 94 | var reader = new FileReader(); 95 | $scope.inputFilename = fileList[0].name; 96 | reader.onload = function () { 97 | var magic = new DataView(this.result).getUint32(0, true /* LE */); 98 | if (magic !== 0x04034b50) { // magic marker per spec. 99 | handleList(); 100 | return; 101 | } 102 | reader.onload = function() { 103 | var zip = new JSZip(this.result); 104 | $scope.inputFiles = zip.file(/\.js$/); 105 | $scope.$apply(); 106 | }; 107 | reader.readAsArrayBuffer(fileList[0]); 108 | }; 109 | reader.readAsArrayBuffer(fileList[0].slice(0, 4)); 110 | }; 111 | 112 | function handleList() { 113 | //uploading individual js file(s) case 114 | $scope.inputFilename="Multiple files"; 115 | var jsType = /(text\/javascript|application\/javascript|application\/x-javascript)/; 116 | var zip = new JSZip(); //create a jszip to manage the files 117 | 118 | for (var i = 0; i < fileList.length; i++) { 119 | var file = fileList[i]; 120 | console.log('adding file:',file.name) 121 | if (!file.type.match(jsType)) { 122 | console.log("Ignoring non-js file:" + file.name + "(" + file.type + ")") 123 | } 124 | var reader = new FileReader(); 125 | reader.readAsText(file); 126 | 127 | reader.onload = (function (file) { 128 | var fileName = file.name; 129 | return function(e){ 130 | //add file to zip 131 | zip.file(fileName, e.target.result) 132 | $scope.inputFiles = zip.file(/.*/); //returns an array of files 133 | $scope.$apply(); 134 | 135 | }; 136 | })(file) 137 | 138 | reader.onerror = function (e) { 139 | $scope.error = "Could not read file"; 140 | $scope.$apply(); 141 | } 142 | } 143 | }; 144 | $scope.throbInput = true; 145 | $scope.$apply(); 146 | //enable fileselect div 147 | //document.querySelector("#scan-intro").classList.toggle("hidden",true); 148 | document.querySelector("#scan-files-selected").classList.toggle("hidden",false); 149 | 150 | if (fileList.length === 1) { 151 | handleMaybeZip(); 152 | } 153 | else { 154 | handleList(); 155 | } 156 | $scope.throbInput = false; 157 | $scope.$apply(); 158 | } 159 | 160 | $scope.showFile = function (index) { 161 | document.querySelector("#code-mirror-wrapper").classList.toggle("hidden",false); 162 | if($scope.inputFiles.length<1){ 163 | return; 164 | } 165 | if(!index){ 166 | index=0; 167 | } 168 | if ($scope.inputFiles.length > 0) { 169 | $scope.codeMirror.setValue($scope.inputFiles[index].asText()); 170 | } 171 | codeMirror_index = index; 172 | document.querySelector("#filename-badge").textContent = $scope.inputFiles[index].name; 173 | } 174 | 175 | $scope.showResult = function (filename,line, col) { 176 | document.querySelector("#code-mirror-wrapper").classList.toggle("hidden",false); 177 | document.querySelector("#filename-badge").textContent = filename; 178 | var file = $scope.inputFiles.find(function(f){return f.name === filename}); 179 | $scope.codeMirror.setValue(file.asText()); 180 | $scope.codeMirror.setCursor(line - 1, col || 0); 181 | $scope.codeMirror.focus(); 182 | }; 183 | 184 | $scope.saveState = function() { 185 | var includedAttributes = ['line','filename','rule', 'desc', 'name', 'rec','type']; 186 | /* A list of attributes we want include. Example: 187 | line: .. 188 | filename: .. 189 | rule: { 190 | desc: .. 191 | name: .. 192 | rec: .. 193 | type: .. 194 | } 195 | } 196 | */ 197 | var serializedResults = JSON.stringify($scope.results, includedAttributes); 198 | localforage.setItem('results', serializedResults, function() { }); 199 | 200 | var serializedErrors = JSON.stringify($scope.errors); 201 | localforage.setItem('errors', serializedErrors, function() { }); 202 | 203 | var serializedInputFiles = $scope.inputFiles.map( function(el) { return {data: el.asText(), name: el.name }; }); 204 | localforage.setItem("inputFiles", JSON.stringify(serializedInputFiles), function(r) { }); 205 | 206 | var checkboxes = []; 207 | for (var i=0; i < $scope.inputFiles.length; i++) { 208 | checkboxes.push(document.getElementById("doScan_" + i).checked); 209 | } 210 | localforage.setItem("checkboxes", JSON.stringify(checkboxes)); 211 | localforage.setItem("cm_index", JSON.stringify(codeMirror_index)); 212 | }; 213 | 214 | //TODO loadstate isn't called anymore, need to make it work with new workflow 215 | //TODO -> call loadState() around in main.js, line 36 (using the scanCtrlScope) and expose "reset" button in the UI. 216 | $scope.restoreState = function() { 217 | var apply = false; 218 | localforage.getItem('results', function (results_storage) { 219 | $scope.results = JSON.parse(results_storage); 220 | apply = true; 221 | }); 222 | localforage.getItem('errors', function (errors_storage) { 223 | if (errors_storage) { 224 | $scope.errors = JSON.parse(errors_storage); 225 | apply = true; 226 | } 227 | }); 228 | // restore files, by creating JSZip things :) 229 | localforage.getItem("inputFiles", function(inputFiles_storage) { 230 | // mimic behavior from handleFileUpload 231 | var files = JSON.parse(inputFiles_storage); 232 | var zip = new JSZip(); 233 | files.forEach(function(file) { 234 | zip.file(file.name, file.data); 235 | }); 236 | $scope.inputFiles = zip.file(/.*/); 237 | 238 | // nest checkbox import into the one for files, so we ensure the "inputFiles.length" check succeeds. 239 | localforage.getItem("checkboxes", function (checkboxes_storage) { 240 | var checkboxes = JSON.parse(checkboxes_storage); 241 | 242 | var ln=$scope.inputFiles.length 243 | for (var i=0; i < ln; i++) { 244 | document.getElementById("doScan_" + i).checked = checkboxes[i]; 245 | } 246 | }); 247 | apply = true; 248 | }); 249 | if (apply) { $scope.$apply(); } 250 | }; 251 | 252 | $scope.selectAll = function () { 253 | var element; 254 | var i = $scope.inputFiles.length-1; 255 | while (element=document.getElementById('doScan_'+i)) { 256 | element.checked = true; 257 | i--; 258 | } 259 | }; 260 | $scope.selectNone = function () { 261 | var element; 262 | var i = $scope.inputFiles.length-1; 263 | while (element=document.getElementById('doScan_'+i)) { 264 | element.checked = false; 265 | i--; 266 | } 267 | }; 268 | $scope.getSnippet = function (filename,line,numLines) { 269 | var file = $scope.inputFiles.find(function (f) { 270 | return f.name === filename 271 | }); 272 | var content=file.asText(); 273 | return content.split('\n').splice(line,line+numLines).join('\n'); 274 | }; 275 | 276 | $scope.$on('NewResults', function (event, result) { 277 | if (--pending <= 0) { $scope.throbOutput = false; } 278 | if (Object.keys(result).length === 0) { 279 | $scope.error = "Empty result set (this can also be a good thing, if you test a simple file)"; 280 | return 281 | } 282 | $scope.results=$scope.results.concat(result.findings); 283 | $scope.filteredResults=$scope.results; 284 | $scope.error = ""; 285 | $scope.updateIssueList(); 286 | /* this is likely a bug in angular or how we use it: the HTML template sometimes does not update 287 | when we change the $scope variables without it noticing. $scope.$apply() enforces this. */ 288 | $scope.$apply(); 289 | $scope.saveState(); 290 | }); 291 | 292 | $scope.$on('ScanError', function (event, exception) { 293 | if (--pending <= 0) { $scope.throbOutput = false; } 294 | $scope.errors.push(exception); 295 | $scope.updateIssueList(); 296 | $scope.$apply(); 297 | $scope.saveState(); 298 | }); 299 | 300 | } 301 | -------------------------------------------------------------------------------- /client/js/scanservice.js: -------------------------------------------------------------------------------- 1 | scanjsModule.factory('ScanSvc', function($rootScope) { 2 | var ScanService = { 3 | //results:[], 4 | ready:false, 5 | rules:null, 6 | init:function(rules){ 7 | this.rules=rules; 8 | this.ready=true; 9 | }, 10 | newScan: function(file,source) { 11 | var fileName = file || 'inline'; 12 | this.scanWorker.postMessage({call: 'scan', arguments: [source, fileName]}); 13 | }, 14 | addResults: function(results) { 15 | $rootScope.$broadcast('NewResults', results); 16 | }, 17 | loadRules:function(ruleData){ 18 | this.rules=ruleData; 19 | this.scanWorker.postMessage({call: 'updateRules', rules: ruleData}); 20 | } 21 | }; 22 | ScanService.scanWorker = new Worker("js/scanworker.js"); 23 | ScanService.scanWorker.addEventListener("message", function (evt) { 24 | if (('findings' in evt.data) && ('filename' in evt.data)) { 25 | if (evt.data.findings.length > 0) { 26 | if (evt.data.findings[0].type == 'error') { 27 | $rootScope.$broadcast('ScanError', evt.data.findings[0]) 28 | return; 29 | } 30 | } 31 | ScanService.addResults(evt.data); 32 | } 33 | else if ('error' in evt.data) { 34 | // This is for errors in the worker, not in the scanning. 35 | // Exceptions (like SyntaxErrors) when scanning files 36 | // are in the findings. 37 | var exception = evt.data.error; 38 | if (e instanceof SyntaxError) { 39 | $rootScope.$broadcast('ScanError', {filename: evt.data.filename, name: exception.name, loc: exception.loc, message: exception.message }) 40 | } else { 41 | throw e; // keep throwing unexpected things. 42 | } 43 | } 44 | }); 45 | 46 | ScanService.scanWorker.onerror = function (e) { console.log('ScanWorker Error: ', e) }; 47 | return ScanService; 48 | }); 49 | -------------------------------------------------------------------------------- /client/js/scanworker.js: -------------------------------------------------------------------------------- 1 | /* this makes sure that console.log can be used, even if it is undefined. 2 | We won't see the message though, since this kind of postMessage isn't handled in scanservice.js */ 3 | if (typeof console === "undefined") { 4 | console = {}; 5 | console.log = function consoleShim(mesg) { 6 | postMessage({'type':'log', 'message': mesg}); 7 | } 8 | } 9 | 10 | importScripts('lib/acorn.js', 11 | 'lib/walk.js', 12 | 'lib/acorn_loose.js', 13 | '../../common/scan.js'); 14 | 15 | //load default rules 16 | //ScanJS.loadRulesFile("../../common/rules.json") 17 | 18 | onmessage = function (evt) { 19 | if (evt.data.call === 'scan') { 20 | var args = evt.data.arguments; 21 | var source = args[0]; 22 | var rules; 23 | 24 | var file = args[1]; 25 | var findings = ScanJS.scan(source,file); 26 | postMessage({"filename": file, "findings": findings}); 27 | } 28 | else if(evt.data.call === 'updateRules'){ 29 | console.log('scanworker.js: Loaded '+evt.data.rules.length+" rules.") 30 | ScanJS.loadRules(evt.data.rules) 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /client/rules.readme.md: -------------------------------------------------------------------------------- 1 | Rules maintained here: 2 | https://docs.google.com/a/mozilla.com/spreadsheets/d/1T14mvMMAspnvhKXxPNRBiV0x6QKZsXr8_b7eXJvEf_A/edit?alt=json#gid=0 3 | 4 | Converted to JSON with http://shancarter.github.io/mr-data-converter/ 5 | 6 | -------------------------------------------------------------------------------- /common/scan.js: -------------------------------------------------------------------------------- 1 | (function (mod) { 2 | if (typeof exports == "object" && typeof module == "object") return mod(exports); // CommonJS 3 | if (typeof define == "function" && define.amd) return define(["exports"], mod); // AMD 4 | mod(this.ScanJS || (this.ScanJS = {})); // Plain browser env 5 | })(function (exports) { 6 | 7 | var rules = {}; 8 | var results = []; 9 | var current_source; 10 | 11 | var aw_found = function (rule, node) { 12 | 13 | results.push({ 14 | type: 'finding', 15 | rule: rule, 16 | filename: results.filename, 17 | line: node.loc.start.line, 18 | col: node.loc.start.col, 19 | // node: node, // add a lot of cruft, removing by default 20 | //this adds a snippet based on lines. Not really useful unless source is prettified first 21 | snippet: current_source.split('\n').splice(node.loc.start.line - 1, node.loc.start.line + 1).join('\n') 22 | }); 23 | 24 | aw_found_callback(rule, node); 25 | } 26 | var aw_found_callback = function () { 27 | }; 28 | 29 | var templateRules = { 30 | identifier: { 31 | nodeType: "Identifier", 32 | test: function (testNode, node) { 33 | if (node.type=="Identifier"&&node.name == testNode.name) { 34 | return true; 35 | } 36 | } 37 | }, 38 | property: { 39 | nodeType: "MemberExpression", 40 | test: function (testNode, node) { 41 | // foo.bar & foo['bar'] create different AST but mean the same thing 42 | 43 | var testName = testNode.property.type == 'Identifier' ? testNode.property.name : testNode.property.value; 44 | if (node.property && (node.property.name == testName || node.property.value == testName)) { 45 | return true; 46 | } 47 | } 48 | }, 49 | objectproperty: { 50 | nodeType: "MemberExpression", 51 | test: function (testNode, node) { 52 | // foo.bar & foo['bar'] create different AST but mean the same thing 53 | var testName = testNode.property.type == 'Identifier' ? testNode.property.name : testNode.property.value; 54 | 55 | if ((node.property && (node.property.name == testName || node.property.value == testName)) && 56 | (node.object.name == testNode.object.name || // this matches the foo in foo.bar 57 | (node.object.property && node.object.property.name == testNode.object.name) )) { //this matches foo in nested objects e.g. baz.foo.bar 58 | return true; 59 | } 60 | } 61 | }, 62 | new: { 63 | nodeType: "NewExpression", 64 | test: function (testNode, node) { 65 | if (node.callee.name == testNode.callee.name) { 66 | return true; 67 | } 68 | } 69 | }, 70 | call: { 71 | nodeType: "CallExpression", 72 | test: function (testNode, node) { 73 | // matches foo() 74 | if (node.callee.type == 'Identifier' && 75 | node.callee.name == testNode.callee.name) { 76 | return true; 77 | } 78 | } 79 | }, 80 | propertycall: { 81 | nodeType: "CallExpression", 82 | test: function (testNode, node) { 83 | if (templateRules.property.test(testNode.callee, node.callee)) { 84 | return true; 85 | } 86 | } 87 | }, 88 | objectpropertycall: { 89 | nodeType: "CallExpression", 90 | test: function (testNode, node) { 91 | if (templateRules.objectproperty.test(testNode.callee, node.callee)) { 92 | return true; 93 | } 94 | } 95 | }, 96 | matchArgs:function(testNode,node){ 97 | var matching = node.arguments.length > 0; 98 | var index = 0; 99 | while (matching && index < testNode.arguments.length) { 100 | var testArg = testNode.arguments[index]; 101 | //ensure each literal argument matches 102 | if (testArg.type == "Literal") { 103 | if (typeof node.arguments[index] == 'undefined' || node.arguments[index].type != "Literal" || testArg.value != node.arguments[index].value) { 104 | matching = false; 105 | } 106 | } 107 | index++; 108 | } 109 | if (matching) { 110 | return true; 111 | } 112 | }, 113 | callargs: { 114 | nodeType: "CallExpression", 115 | test: function (testNode, node) { 116 | if (templateRules.call.test(testNode,node) && 117 | templateRules.matchArgs(testNode,node)) { 118 | return true; 119 | } 120 | } 121 | }, 122 | propertycallargs: { 123 | nodeType: "CallExpression", 124 | test: function (testNode, node) { 125 | if (templateRules.propertycall.test(testNode,node) && 126 | templateRules.matchArgs(testNode,node)) { 127 | return true; 128 | } 129 | } 130 | }, 131 | objectpropertycallargs: { 132 | nodeType: "CallExpression", 133 | test: function (testNode, node) { 134 | if (templateRules.objectpropertycall.test(testNode,node) && 135 | templateRules.matchArgs(testNode,node)) { 136 | return true; 137 | } 138 | } 139 | }, 140 | assignment: { 141 | nodeType: "AssignmentExpression", 142 | test: function (testNode, node) { 143 | if (templateRules.identifier.test(testNode.left,node.left)) { 144 | return true; 145 | } 146 | } 147 | }, 148 | propertyassignment: { 149 | nodeType: "AssignmentExpression", 150 | test: function (testNode, node) { 151 | if (templateRules.property.test(testNode.left,node.left)) { 152 | return true; 153 | } 154 | } 155 | }, 156 | objectpropertyassignment: { 157 | nodeType: "AssignmentExpression", 158 | test: function (testNode, node) { 159 | if (templateRules.objectproperty.test(testNode.left,node.left)) { 160 | return true; 161 | } 162 | } 163 | } 164 | }; 165 | 166 | function aw_loadRulesFile(rulesFile, callback) { 167 | 168 | var request = new XMLHttpRequest(); 169 | 170 | request.open('GET', rulesFile, false); 171 | 172 | request.onload = function () { 173 | if (request.status >= 200 && request.status < 400) { 174 | rulesData = JSON.parse(request.responseText); 175 | aw_loadRules(rulesData); 176 | if (typeof callback == 'function') 177 | callback(rules); 178 | } else { 179 | console.log('Error loading ' + rules) 180 | } 181 | }; 182 | 183 | request.onerror = function () { 184 | console.log('Connection error while loading ' + rulesFile) 185 | }; 186 | request.send(); 187 | } 188 | 189 | 190 | function aw_parseRule(rule) { 191 | try { 192 | var program = acorn.parse(rule.source); 193 | //each rule must contain exactly one javascript statement 194 | if (program.body.length != 1) { 195 | throw ('Rule ' + rule.name + 'contains too many statements, skipping: '+ rule.source); 196 | 197 | } 198 | rule.statement = program.body[0] 199 | } catch (e) { 200 | throw('Can\'t parse rule:' + rule.name ); 201 | } 202 | 203 | //identifier 204 | if (rule.statement.expression.type == "Identifier") { 205 | return 'identifier'; 206 | } 207 | 208 | if (rule.statement.expression.type == "NewExpression") { 209 | return 'new'; 210 | } 211 | 212 | //property, objectproperty 213 | if (rule.statement.expression.type == "MemberExpression") { 214 | if (rule.statement.expression.object.name == "$") { 215 | //rule is $.foo, this is a property rule 216 | return 'property'; 217 | } else { 218 | return 'objectproperty'; 219 | } 220 | } 221 | //call, propertycall,objectpropertycall ( + args variants) 222 | if (rule.statement.expression.type == "CallExpression") { 223 | var args = ''; 224 | if (rule.statement.expression.arguments.length > 0) { 225 | args = 'args'; 226 | } 227 | 228 | if (rule.statement.expression.callee.type == "Identifier") { 229 | return 'call' + args; 230 | } else if (rule.statement.expression.callee.type == "MemberExpression") { 231 | if (rule.statement.expression.callee.object.name == "$") { 232 | return 'propertycall' + args; 233 | } else { 234 | return 'objectpropertycall' + args; 235 | } 236 | } 237 | } 238 | 239 | //assignment, propertyassignment, objectpropertyassignment 240 | if (rule.statement.expression.type == "AssignmentExpression") { 241 | if (rule.statement.expression.left.type == "MemberExpression") { 242 | if (rule.statement.expression.left.object.name == "$") { 243 | return 'propertyassignment'; 244 | } else { 245 | return 'objectpropertyassignment'; 246 | } 247 | } else { 248 | return 'assignment'; 249 | } 250 | } 251 | 252 | 253 | 254 | //if we get to here we couldn't find a matching template for the rule.source 255 | throw ("No matching template") 256 | } 257 | 258 | function aw_loadRules(rulesData) { 259 | 260 | var nodeTests = {}; 261 | //each node type may have multiple tests, so first create arrays of test funcs 262 | for (i in rulesData) { 263 | var rule = rulesData[i]; 264 | //parse rule source 265 | var template; 266 | try { 267 | template = templateRules[aw_parseRule(rule)]; 268 | } catch (e) { 269 | console.log("Error parsing rule " + rule.name, rule.source) 270 | break; 271 | } 272 | 273 | if (typeof template == 'undefined') { 274 | console.log("Should never get here.") 275 | break; 276 | } 277 | 278 | if (!nodeTests[template.nodeType]) { 279 | nodeTests[template.nodeType] = []; 280 | } 281 | nodeTests[template.nodeType].push(function (template, rule, node) { 282 | if (template.test(rule.statement.expression, node)) { 283 | aw_found(rule, node); 284 | } 285 | }.bind(undefined, template, rule)); 286 | } 287 | 288 | 289 | rules = {}; 290 | //create a single function for each nodeType, which calls all the test functions 291 | for (nodeType in nodeTests) { 292 | rules[nodeType] = function (tests, node) { 293 | tests.forEach(function (test) { 294 | test.call(this, node); 295 | }); 296 | }.bind(undefined, nodeTests[nodeType]); 297 | } 298 | } 299 | 300 | function aw_scan(source, filename) { 301 | results = []; 302 | results.filename = "Manual input" 303 | 304 | current_source = source; 305 | 306 | if (typeof filename != 'undefined') { 307 | results.filename = filename; 308 | } 309 | var ast; 310 | try { 311 | ast = acorn.parse(source, { 312 | locations: true 313 | }); 314 | } catch (e) { 315 | return [ 316 | { 317 | type: 'error', 318 | name: e.name, 319 | pos: e.pos, 320 | loc: { column: e.loc.column, line: e.loc.line }, 321 | message: e.message, 322 | filename: filename 323 | } 324 | ]; 325 | } 326 | 327 | 328 | if (!rules) { 329 | return [ 330 | { 331 | type: 'error', 332 | name: 'RulesError', 333 | pos: 0, 334 | loc: { column: 0, line: 0 }, 335 | message: "Could not scan with no rules loaded.", 336 | filename: filename 337 | } 338 | ]; 339 | } 340 | acorn.walk.simple(ast, rules); 341 | 342 | return results; 343 | } 344 | 345 | function aw_setCallback(found_callback) { 346 | aw_found_callback = found_callback; 347 | } 348 | 349 | exports.rules = rules; 350 | exports.scan = aw_scan; 351 | exports.loadRulesFile = aw_loadRulesFile; 352 | exports.loadRules = aw_loadRules; 353 | exports.parseRule = aw_parseRule; 354 | exports.setResultCallback = aw_setCallback; 355 | 356 | }); -------------------------------------------------------------------------------- /common/template_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "identifier", 4 | "source": "foo", 5 | "testhit": "foo;", 6 | "testmiss": "'foo';", 7 | "desc": "Matches identifier with same name.", 8 | "threat": "example" 9 | }, 10 | { 11 | "name": "property", 12 | "source": "$.foo", 13 | "testhit": "a.foo; a.b.foo; this['foo'];this['bar'].foo;", 14 | "testmiss": "foo;", 15 | "desc": "Matches object with property of same name.", 16 | "threat": "example" 17 | }, 18 | { 19 | "name": "objectproperty", 20 | "source": "foo.bar", 21 | "testhit": "foo.bar; a.foo.bar; foo['bar'];", 22 | "testmiss": "foo.foo;bar.bar;", 23 | "desc": "Matches object name and property name.", 24 | "threat": "example" 25 | }, 26 | { 27 | "name": "call", 28 | "source": "foo()", 29 | "testhit": "foo();", 30 | "testmiss": "foo;", 31 | "desc": "Matches funtion where callee has a given name.", 32 | "threat": "example" 33 | }, 34 | { 35 | "name": "propertycall", 36 | "source": "$.foo()", 37 | "testhit": "a.foo(); a.b.foo(); this['foo']();", 38 | "testmiss": "foo();foo;a.foo;", 39 | "desc": "Find function, where callee is a member object, matching property name only", 40 | "threat": "example" 41 | }, 42 | { 43 | "name": "objectpropertycall", 44 | "source": "foo.bar()", 45 | "testhit": "foo.bar(); a.foo.bar(); foo['bar']();", 46 | "testmiss": "foo.foo();bar.bar();foo.bar;", 47 | "desc": "Match function, where callee is a member object, matching object name and propertyname", 48 | "threat": "example" 49 | }, 50 | { 51 | "name": "callargs", 52 | "source": "foo('test',ignored,42)", 53 | "testhit": "foo('test',foo,42); foo('test',foo,42,'extra'); ", 54 | "testmiss": "foo; foo(); a.foo('test',foo,42); foo('wrong',foo,42); foo('test',foo,41); foo('test',41); foo(foo,'test', foo,42)", 55 | "desc": "Same as for call, expect this allow makes sure that any supplied literal arguments are matching (both in value, and in position)", 56 | "threat": "example" 57 | }, 58 | { 59 | "name": "propertycallargs", 60 | "source": "$.foo('test',ignored,42)", 61 | "testhit": "$.foo('test',foo,42); $.foo('test',foo,42,'extra'); $.a.foo('test',foo,42); $.a[foo]('test',foo,42); $.a['b'].foo('test',bar,42);", 62 | "testmiss": "foo('test',foo,42); a.foo('miss',foo,42); a.foo('test',foo); a[foo]('test',foo,41); a['b'].foo('test',42); foo('miss'); a[foo](foo,41);", 63 | "desc": "Same as for propertycall, expect this allow makes sure that any supplied literal arguments are matching (both in value, and in position)", 64 | "threat": "example" 65 | }, 66 | { 67 | "name": "objectpropertycallargs", 68 | "source": "foo.bar('test',ignored,42)", 69 | "testhit": "foo.bar('test',foo,42); foo.bar('test',foo,42,'extra'); a.foo.bar('test',foo,42); a[foo]['bar']('test',foo,42); a['b'].foo.bar('test',bar,42);", 70 | "testmiss": "foo; foo(); foo('miss',foo,42); foo.foo('test',foo,42); bar.bar('test',foo,42)", 71 | "desc": "Same as for objectpropoertycall, expect this allow makes sure that any supplied literal arguments are matching (both in value, and in position)", 72 | "threat": "example" 73 | }, 74 | { 75 | "name": "assignment", 76 | "source": "foo=$", 77 | "testhit": "foo=bar;", 78 | "testmiss": "foo; foo", 79 | "desc": "Assigning to identifier with given name", 80 | "threat": "example" 81 | }, 82 | { 83 | "name": "propertyassignment", 84 | "source": "$.foo=$", 85 | "testhit": "a.foo=bar; a.b.foo=bar; this['foo']=bar;", 86 | "testmiss": "foo=bar", 87 | "desc": "Assigning to a object property with a given name", 88 | "threat": "example" 89 | }, 90 | { 91 | "name": "objectpropertyassignment", 92 | "source": "foo.bar=$", 93 | "testhit": "foo.bar=a; a.foo.bar=b; foo['bar']=c;a.foo['bar']=c;", 94 | "testmiss": "bar.bar=a;foo.foo=b;", 95 | "desc": "Assigning to a object property matching both object name and property name", 96 | "threat": "example" 97 | } 98 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scanjs", 3 | "version": "0.0.2", 4 | "description": "Static analysis tool for javascript codebases", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mozilla/scanjs.git" 9 | }, 10 | "keywords": [ 11 | "scanjs", 12 | "code-analysis", 13 | "javascript", 14 | "static-analysis" 15 | ], 16 | "author": "Paul Theriault, Frederik Braun, Rob Fletcher", 17 | "license": "MPL", 18 | "dependencies": { 19 | "acorn": "~0.3.0", 20 | "tern": "~0.5.0", 21 | "optimist": "~0.5.2", 22 | "jszip": "~2.2.0", 23 | "node-static": "~0.7.3", 24 | "js-beautify":"~1.4.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scanner.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | // This script is for using scanjs server side 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var beautify = require('js-beautify').js_beautify; 8 | global.acorn = require(__dirname + '/client/js/lib/acorn.js'); 9 | acorn.walk = require('acorn/util/walk.js'); 10 | 11 | var ScanJS = require(__dirname + '/common/scan'); 12 | var signatures = JSON.parse(fs.readFileSync(__dirname + "/common/rules.json", "utf8")); 13 | ScanJS.loadRules(signatures); 14 | 15 | var argv = require('optimist').usage('Usage: $node scan.js -t [path/to/app] -o [resultFile.json]').demand(['t']).argv; 16 | 17 | var dive = function(dir, action) { 18 | if( typeof action !== 'function') { 19 | action = function(error, file) { 20 | console.log(">" + file) 21 | }; 22 | } 23 | list = fs.readdirSync(dir); 24 | list.forEach(function(file) { 25 | var fullpath = dir + '/' + file; 26 | try { 27 | var stat = fs.statSync(fullpath); 28 | } catch(e) { 29 | console.log("SKIPPING FILE: Could not stat " + fullpath); 30 | } 31 | if(stat && stat.isDirectory()) { 32 | dive(fullpath, action); 33 | } else { 34 | action(file, fullpath); 35 | } 36 | }); 37 | }; 38 | var writeReport = function(results, name) { 39 | if(fs.existsSync(name)) { 40 | console.log("Error:output file already exists (" + name + "). Supply a different name using: -o [filename]") 41 | } 42 | fs.writeFile(name, JSON.stringify(results), function(err) { 43 | if(err) { 44 | console.log(err); 45 | } else { 46 | console.log("The scan results were saved to " + name); 47 | } 48 | }); 49 | }; 50 | 51 | 52 | if( typeof process != 'undefined' && process.argv[2]) { 53 | results = {}; 54 | reportname = argv.o ? argv.o : 'scanresults'; 55 | reportdir = reportname + "_files"; 56 | if(fs.existsSync(reportname) || fs.existsSync(reportdir)) { 57 | console.log("Error:output file or dir already exists (" + reportname + "). Supply a different name using: -o [filename]") 58 | 59 | } 60 | else { 61 | fs.mkdirSync(reportdir); 62 | dive(argv.t, function(file, fullpath) { 63 | var ext = path.extname(file.toString()); 64 | 65 | if(ext == '.js') { 66 | var content = fs.readFileSync(fullpath, 'utf8'); 67 | //beautify source so result snippet is meaningful 68 | var content = beautify(content, { indent_size: 2 }) 69 | var scanresult = ScanJS.scan(content, fullpath); 70 | if (scanresult.type == 'error') { 71 | console.log("SKIPPING FILE: Error in "+ fullpath+", at Line "+ scanresult.error.loc.line +", Column "+scanresult.error.loc.column+ ": " + scanresult.error.message); 72 | } 73 | results[fullpath] = scanresult; 74 | } 75 | }); 76 | // Flatten report file to remove files with no findings and tests with no results (i.e. empty arr) 77 | // TODO: Don't even store empty unless --overly-verbose or so.. 78 | for (var testedFile in results) { 79 | for (var testCase in results[testedFile]) { 80 | if (results[testedFile][testCase].length == 0) { 81 | delete(results[testedFile][testCase]); 82 | } 83 | } 84 | if (Object.keys(results[testedFile]).length == 0) { 85 | delete(results[testedFile]); 86 | } 87 | } 88 | writeReport(results, reportname + '.JSON'); 89 | } 90 | } else { 91 | console.log('usage: $ node scan.js path/to/app ') 92 | } 93 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var static = require("node-static"); 2 | var file = new static.Server('.', { 3 | headers: { 4 | "Content-Security-Policy": "default-src 'self'; object-src 'none'; img-src 'self' data:; script-src 'self' 'unsafe-eval'", 5 | } 6 | }); 7 | 8 | const PORT = process.env.PORT || 4000; 9 | require ('http').createServer(function (req, res) { 10 | req.addListener('end', function () { 11 | file.serve(req, res); 12 | }).resume(); 13 | }).listen(PORT); 14 | 15 | console.log("> node-static is listening on http://127.0.0.1:"+PORT); 16 | -------------------------------------------------------------------------------- /stackato.yml: -------------------------------------------------------------------------------- 1 | name: ScanJS 2 | framework: 3 | type: node 4 | runtime: node010 5 | 6 | url: 7 | - scanjs.paas.allizom.org 8 | 9 | processes: 10 | web: node server.js 11 | -------------------------------------------------------------------------------- /tests/advanced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scan.js Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

Scan.js Mocha Unit Tests

25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tests/cases/CustomEvent.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('CustomEvent tests - future rule?', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "CustomEvent";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good.CustomEvent = "CustomEvent";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'obj.addEventListener("cat", function(e) { process(e.detail) }); var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}}); obj.dispatchEvent(event);'; 20 | it.skip(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /tests/cases/action.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('action attribute (mainly for forms) test', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var action = "static string";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function () { 12 | context(null, function () { 13 | var bad = 'var a=document.createElement("form"); a.action="demo_form.asp"; document.body.appendChild(a);'; 14 | it(bad, function () { 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 20 | var bad = 'var a=document.createElement("form"); a.setAttribute("action", "demo_form.asp"); document.body.appendChild(a);'; 21 | it.skip(bad, function () { 22 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 23 | }); 24 | }); 25 | context(null, function () { 26 | // issue 74 - https://github.com/mozilla/scanjs/issues/74 27 | var bad = 'var a=document.createElement("div"); a.innerHTML="
"; document.body.appendChild(a);'; 28 | it.skip(bad, function () { 29 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 30 | }); 31 | }); 32 | }); 33 | }); 34 | })(); 35 | -------------------------------------------------------------------------------- /tests/cases/addEventListener.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('addEventListener tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "addEventListener";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var addEventListener = "variable with name";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'var el = document.getElementById("outside");el.addEventListener("click", modifyText, false);'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'addEventListener("click", errorPageEventHandler, true, false);'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | var bad = 'tab.linkedBrowser.addEventListener("load", function (event) {console.log(1);});'; 32 | it(bad, function () { 33 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 34 | }); 35 | }); 36 | }); 37 | }); 38 | })(); 39 | -------------------------------------------------------------------------------- /tests/cases/addIdleObserver.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('addIdleObserver tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "addIdleObserver()";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'addIdleObserver = "static string";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'something.addIdleObserver = "static string";'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | }); 23 | context('detects dangerous patterns', function () { 24 | context(null, function () { 25 | var bad = 'navigator.addIdleObserver(IdleObserver);'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /tests/cases/createContextualFragment.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('createContextualFragment tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "createContextualFragment()";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var createContextualFragment = "static string";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'var documentFragment = range.createContextualFragment("

bad

");'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /tests/cases/crypto.generateCRMFRequest.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('generateCRMFRequest tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'generateCRMFRequest = "static string";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = "generateCRMFRequest";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'crypto.generateCRMFRequest("CN=0", 0, 0, null, "console.log(1)", 384, null, "rsa-dual-use");;'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 26 | var bad = 'var a = crypto; a.generate = crypto.generateCRMFRequest; a.generate("CN=0", 0, 0, null, "console.log(1)", 384, null, "rsa-dual-use");'; 27 | it.skip(bad, function () { 28 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /tests/cases/data.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('data attribute tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "something.data";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var data = "something";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'form.data = "mystring";'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | }); 23 | context('detects dangerous patterns', function () { 24 | context(null, function () { 25 | var bad = 'var a = event.data;'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | var bad = 'readPipe(event.something.data.pipe, a, b);'; 32 | it(bad, function () { 33 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 34 | }); 35 | }); 36 | context(null, function () { 37 | var bad = 'readPipe(event.data.pipe, a, b);'; 38 | it(bad, function () { 39 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 40 | }); 41 | }); 42 | }); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /tests/cases/document.write.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('document.write tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'good.write = "static string";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good = "document.write";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'document.write("Hello World!");'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'window.document.write("Hello World!");'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 32 | var bad = 'var a = window.document; a.b = document.write; a.b("

bad

");'; 33 | it.skip(bad, function () { 34 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /tests/cases/document.writeln.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('document.writeln tests', function () { 3 | describe('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'good.writeln = "static string";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good = "document.writeln";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | describe('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'document.writeln("Hello World!");'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'window.document.writeln("Hello World!");'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 32 | var bad = 'var a = window.document; a.b = document.writeln; a.b("

bad

");'; 33 | it.skip(bad, function () { 34 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /tests/cases/escapeHTML.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('escapeHTML tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "escapeHTML";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var escapeHTML = "just a string";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'var nodeName = escapeHTML(node.name);'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /tests/cases/eval.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('eval tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function() { 5 | var good = 'var a = "eval(alert(1));";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function() { 11 | var good = 'var a = {}; a.eval = "somstring";'; 12 | it(good, function(){ 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function() { 18 | context(null, function() { 19 | var bad = 'eval("alert(0);");;'; 20 | it(bad, function(){ 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function() { 25 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 26 | var bad = 'var a = eval; a("alert(0);");'; 27 | it.skip(bad, function(){ 28 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /tests/cases/geolocation.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('geolocation tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function () { 5 | var good = 'var a = "navigator.geolocation.getCurrentPosition(success, error);";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function() { 12 | context(null, function () { 13 | var bad = 'navigator.geolocation.getCurrentPosition(showPosition);'; 14 | it(bad, function(){ 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | var bad = 'var a = navigator.geolocation; a.getCurrentPosition(showPosition);'; 20 | it(bad, function(){ 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'var a = navigator; a.geolocation.getCurrentPosition(showPosition);'; 26 | it.skip(bad, function(){ 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /tests/cases/getDeviceStorage.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('getDeviceStorage tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function () { 5 | var good = 'var a = "navigator.getDeviceStorage(storageName)";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function() { 12 | context(null, function () { 13 | var bad = 'var instanceOfDeviceStorage = navigator.getDeviceStorage(storageName);'; 14 | it(bad, function(){ 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | var bad = 'var a = navigator; a.getDeviceStorage(storageName);'; 20 | it(bad, function(){ 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | // issue 82 - https://github.com/mozilla/scanjs/issues/82 26 | var bad = 'window["navigator"]["getDeviceStorage"](storageName);'; 27 | it.skip(bad, function(){ 28 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /tests/cases/href.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('href tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function () { 5 | var good = 'var href = "static string";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | 12 | }); 13 | context(null, function () { 14 | var good = 'var a = document.createElement("a"); a.setAttribute("href", "http://mozilla.org"); document.body.appendChild(a);'; 15 | it(good, function(){ 16 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 17 | }); 18 | }); 19 | context(null, function () { 20 | var good = 'var a = document.createElement("a"); a.setAttribute("href", 1); document.body.appendChild(a);'; 21 | it(good, function(){ 22 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 23 | }); 24 | }); 25 | }); 26 | context('detects dangerous patterns', function() { 27 | context(null, function () { 28 | var bad = 'a.href ="javascript:alert(0);";'; 29 | it(bad, function(){ 30 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 31 | }); 32 | }); 33 | context(null, function () { 34 | var bad = 'a.href ="data:alert(0);";'; 35 | it(bad, function(){ 36 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 37 | }); 38 | }); 39 | context(null, function () { 40 | var bad = 'something.a.href ="data:alert(0);";'; 41 | it(bad, function(){ 42 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 43 | }); 44 | }); 45 | context(null, function () { 46 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 47 | var bad = 'var a = document.createElement("a"); a.setAttribute("href", "javascript:alert(0)"); document.body.appendChild(a);'; 48 | it.skip(bad, function(){ 49 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 50 | }); 51 | }); 52 | }); 53 | }); 54 | })(); 55 | -------------------------------------------------------------------------------- /tests/cases/indexedDB.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('indexedDB tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function() { 5 | var good = 'var a = "indexedDB.open(abase)";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function() { 12 | context(null, function() { 13 | var bad = 'var request = indexedDB.open("MyTestDatabase");'; 14 | it(bad, function(){ 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function() { 19 | // issue 82 - https://github.com/mozilla/scanjs/issues/82 20 | var bad = 'var a = "indexedDB"; window[a].open(3);'; 21 | it.skip(bad, function(){ 22 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 23 | }); 24 | }); 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /tests/cases/innerhtml.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('innerHTML tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'good.innerHTML = "static string";'; 6 | it.skip(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = document.createElement("div"); a.setAttribute("innerHTML", "

bad

"); document.body.appendChild(a);'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'var getInnerHtml = document.getElementById("node").innerHTML;'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | context(null, function () { 23 | var good = '//div.innerHTML = this is a comment'; 24 | it(good, function () { 25 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 26 | }); 27 | }); 28 | context(null, function () { 29 | var good = 'div.innerHTML = 1'; 30 | it.skip(good, function () { 31 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 32 | }); 33 | }); 34 | context(null, function () { 35 | //issue 71 - https://github.com/mozilla/scanjs/issues/71 36 | var good = 'var a = 1; div.innerHTML = a'; 37 | it.skip(good, function () { 38 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 39 | }); 40 | }); 41 | }); 42 | context('detects dangerous patterns', function () { 43 | context(null, function () { 44 | var bad = 'dangerous.innerHTML=document.location;'; 45 | it(bad, function () { 46 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 47 | }); 48 | }); 49 | context(null, function () { 50 | var bad = 'div.innerHTML = "static string" + someVariable;'; 51 | it(bad, function () { 52 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 53 | }); 54 | }); 55 | }); 56 | }); 57 | })(); 58 | -------------------------------------------------------------------------------- /tests/cases/localStorage.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('localStorage tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function() { 5 | var good = 'var a = "localStorage.open(abase)";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function() { 11 | var good = 'var localStorage = "asdf";'; 12 | it(good, function(){ 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function() { 18 | context(null, function() { 19 | var bad = 'localStorage.setItem("name", "user1");'; 20 | it(bad, function(){ 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function() { 25 | // issue 82 - https://github.com/mozilla/scanjs/issues/82 26 | var bad = 'var a = "localStorage"; window[a].setItem("name", "user1");'; 27 | it.skip(bad, function(){ 28 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /tests/cases/message.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('message tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function () { 5 | var good = 'var a = "message";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var message = "static string";'; 12 | it(good, function(){ 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'function receiveMessage() { console.log(1); }'; 18 | it(good, function(){ 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | context(null, function () { 23 | var good = 'function message() { console.log(1); }'; 24 | it(good, function(){ 25 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 26 | }); 27 | }); 28 | }); 29 | context('detects dangerous patterns', function() { 30 | context(null, function () { 31 | var bad = 'window.addEventListener("message", receiveMessage, false);'; 32 | it(bad, function(){ 33 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 34 | }); 35 | }); 36 | }); 37 | }); 38 | })(); 39 | -------------------------------------------------------------------------------- /tests/cases/moz/moz.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('Mozilla specific tests', function () { 3 | context('MozActivity', function () { 4 | context('safe', function () { 5 | context(null, function () { 6 | var good = 'var a = "static MozActivity";'; 7 | it(good, function () { 8 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 9 | }); 10 | }); 11 | context(null, function () { 12 | var good = 'var MozActivity = "static MozActivity";'; 13 | it(good, function () { 14 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 15 | }); 16 | }); 17 | }); 18 | context('dangerous', function () { 19 | context(null, function () { 20 | var bad = 'var activity = new MozActivity({ name: "pick",data: {type: "image/jpeg"}});'; 21 | it(bad, function () { 22 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 23 | }); 24 | }); 25 | }); 26 | }); 27 | 28 | context('.mozAlarms', function () { 29 | context('safe', function () { 30 | context(null, function () { 31 | var good = 'var mozAlarms = "just a string, not .mozAlarms";'; 32 | it(good, function () { 33 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 34 | }); 35 | }); 36 | }); 37 | context('dangerous', function () { 38 | context(null, function () { 39 | var bad = 'var bad = window.navigator.mozAlarms;'; 40 | it(bad, function () { 41 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | 47 | context('mozApps.mgmt', function () { 48 | context('safe', function () { 49 | context(null, function () { 50 | var good = 'var getAll = "mozApps.mgmt.getAll()";'; 51 | it(good, function () { 52 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 53 | }); 54 | }); 55 | }); 56 | context('dangerous', function () { 57 | context(null, function () { 58 | var bad = 'var stuffs = navigator.mozApps.mgmt;'; 59 | it(bad, function () { 60 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 61 | }); 62 | }); 63 | context(null, function () { 64 | var bad = 'var stuffs = navigator.mozApps.mgmt.getAll()'; 65 | it(bad, function () { 66 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | context('.mozBluetooth', function () { 73 | context('safe', function () { 74 | context(null, function () { 75 | var good = 'var mozBluetooth = "just a string, not .mozBluetooth";'; 76 | it(good, function () { 77 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 78 | }); 79 | }); 80 | }); 81 | context('dangerous', function () { 82 | context(null, function () { 83 | var bad = 'var bad = window.navigator.mozBluetooth;'; 84 | it(bad, function () { 85 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | context('mozCameras', function () { 92 | context('safe', function () { 93 | context(null, function () { 94 | var good = 'var mozCameras = "just a string, not mozCameras";'; 95 | it(good, function () { 96 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 97 | }); 98 | }); 99 | }); 100 | context('dangerous', function () { 101 | context(null, function () { 102 | var bad = 'var bad = window.navigator.mozCameras;'; 103 | it(bad, function () { 104 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 105 | }); 106 | }); 107 | }); 108 | }); 109 | 110 | 111 | context('dangerous', function () { 112 | context(null, function () { 113 | var bad = 'var bad = window.navigator.mozCellBroadcast;'; 114 | it(bad, function () { 115 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | context('mozChromeEvent', function () { 122 | context('safe', function () { 123 | context(null, function () { 124 | var good = 'var a = "string mozChromeEvent";'; 125 | it(good, function () { 126 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 127 | }); 128 | }); 129 | context(null, function () { 130 | var good = 'var mozChromeEvent = "string mozChromeEvent";'; 131 | it(good, function () { 132 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 133 | }); 134 | }); 135 | }); 136 | context('dangerous', function () { 137 | context(null, function () { 138 | var bad = 'window.addEventListener("mozChromeEvent", function (e) {console.log("mozilla rocks!") });'; 139 | it(bad, function () { 140 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | context('.mozContacts', function () { 147 | context('safe', function () { 148 | context(null, function () { 149 | var good = 'var mozContacts = "just a string, not .mozContacts";'; 150 | it(good, function () { 151 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 152 | }); 153 | }); 154 | }); 155 | context('dangerous', function () { 156 | context(null, function () { 157 | var bad = 'var bad = window.navigator.mozContacts;'; 158 | it(bad, function () { 159 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 160 | }); 161 | }); 162 | }); 163 | }); 164 | 165 | context('mozFMRadio', function () { 166 | context('safe', function () { 167 | context(null, function () { 168 | var good = 'var a = "string mozFMRadio;";'; 169 | it(good, function () { 170 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 171 | }); 172 | }); 173 | }); 174 | context('dangerous', function () { 175 | context(null, function () { 176 | var bad = 'var WebFM = navigator.mozFMRadio;'; 177 | it(bad, function () { 178 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 179 | }); 180 | }); 181 | }); 182 | }); 183 | 184 | context('mozKeyboard', function () { 185 | context('safe', function () { 186 | context(null, function () { 187 | var good = 'var a = "mozKeyboard";'; 188 | it(good, function () { 189 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 190 | }); 191 | }); 192 | }); 193 | context('dangerous', function () { 194 | context(null, function () { 195 | var bad = 'mozKeyboard.onfocuschange = alert(0);'; 196 | it(bad, function () { 197 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 198 | }); 199 | }); 200 | }); 201 | }); 202 | 203 | context('mozMobileConnection', function () { 204 | context('safe', function () { 205 | context(null, function () { 206 | var good = 'var a = "mozMobileConnection";'; 207 | it(good, function () { 208 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 209 | }); 210 | }); 211 | }); 212 | context('dangerous', function () { 213 | context(null, function () { 214 | var bad = 'MozMobileConnection.sendMMI()'; 215 | it(bad, function () { 216 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 217 | }); 218 | }); 219 | }); 220 | }); 221 | 222 | context('.mozNotification', function () { 223 | context('safe', function () { 224 | context(null, function () { 225 | var good = 'var mozNotification = ".mozNotification, this is just a string";'; 226 | it(good, function () { 227 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 228 | }); 229 | }); 230 | }); 231 | context('dangerous', function () { 232 | context(null, function () { 233 | var bad = "if (window.webkitNotifications) { _popup = window; } else if (navigator.mozNotification) { console.log (1); }"; 234 | it(bad, function () { 235 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 236 | }); 237 | }); 238 | }); 239 | }); 240 | 241 | context('mozPermissionSettings', function () { 242 | context('safe', function () { 243 | context(null, function () { 244 | var good = 'var a = "just a string, not mozPermissionSettings";'; 245 | it(good, function () { 246 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 247 | }); 248 | }); 249 | context(null, function () { 250 | var good = 'var mozPermissionSettings = "just a string, not mozPermissionSettings";'; 251 | it(good, function () { 252 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 253 | }); 254 | }); 255 | }); 256 | context('dangerous', function () { 257 | context(null, function () { 258 | var bad = 'var permissions = window.navigator.mozPermissionSettings;'; 259 | it(bad, function () { 260 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 261 | }); 262 | }); 263 | }); 264 | }); 265 | 266 | context('mozPower', function () { 267 | context('safe', function () { 268 | context(null, function () { 269 | var good = 'var a = "just a string, window.navigator.mozPower;";'; 270 | it(good, function () { 271 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 272 | }); 273 | }); 274 | context(null, function () { 275 | var good = 'var mozPower = "just a string, window.navigator.mozPower;";'; 276 | it(good, function () { 277 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 278 | }); 279 | }); 280 | }); 281 | context('dangerous', function () { 282 | context(null, function () { 283 | var bad = 'var power = window.navigator.mozPower;'; 284 | it(bad, function () { 285 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 286 | }); 287 | }); 288 | }); 289 | }); 290 | 291 | context('mozSetMessageHandler', function () { 292 | context('safe', function () { 293 | context(null, function () { 294 | var good = 'var a = "static mozSetMessageHandler";'; 295 | it(good, function () { 296 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 297 | }); 298 | }); 299 | }); 300 | context('dangerous', function () { 301 | context(null, function () { 302 | var bad = 'navigator.mozSetMessageHandler(type, handler);'; 303 | it(bad, function () { 304 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 305 | }); 306 | }); 307 | }); 308 | }); 309 | 310 | context('mozSettings', function () { 311 | context('safe', function () { 312 | context(null, function () { 313 | var good = 'var a = "window.navigator.mozSettings;";'; 314 | it(good, function () { 315 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 316 | }); 317 | }); 318 | context(null, function () { 319 | var good = 'var mozSettings = "window.navigator.mozSettings;";'; 320 | it(good, function () { 321 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 322 | }); 323 | }); 324 | }); 325 | context('dangerous', function () { 326 | context(null, function () { 327 | var bad = 'var settings = window.navigator.mozSettings;'; 328 | it(bad, function () { 329 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 330 | }); 331 | }); 332 | }); 333 | }); 334 | 335 | context('mozSms', function () { 336 | context('safe', function () { 337 | context(null, function () { 338 | var good = 'var a = "window.navigator.mozSms;";'; 339 | it(good, function () { 340 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 341 | }); 342 | }); 343 | context(null, function () { 344 | var good = 'var mozSms = "window.navigator.mozSms;";'; 345 | it(good, function () { 346 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 347 | }); 348 | }); 349 | }); 350 | context('dangerous', function () { 351 | context(null, function () { 352 | var bad = 'var sms = window.navigator.mozSms;'; 353 | it(bad, function () { 354 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 355 | }); 356 | }); 357 | context(null, function () { 358 | var bad = 'var msg = window.navigator.mozSms.getMessage(1);'; 359 | it(bad, function () { 360 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 361 | }); 362 | }); 363 | }); 364 | }); 365 | 366 | context('mozSystem', function () { 367 | context('safe', function () { 368 | context(null, function () { 369 | var good = 'var a = "mozSystem: true";'; 370 | it(good, function () { 371 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 372 | }); 373 | }); 374 | }); 375 | context('dangerous', function () { 376 | context(null, function () { 377 | var bad = 'var xhr = new XMLHttpRequest({ mozSystem: true});'; 378 | it(bad, function () { 379 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 380 | }); 381 | }); 382 | }); 383 | }); 384 | 385 | context('.mozTCPSocket', function () { 386 | context('safe', function () { 387 | context(null, function () { 388 | var good = 'var a = "navigator.mozTCPSocket;"'; 389 | it(good, function () { 390 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 391 | }); 392 | }); 393 | }); 394 | context('dangerous', function () { 395 | context(null, function () { 396 | var bad = 'var socket = navigator.mozTCPSocket;'; 397 | it(bad, function () { 398 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 399 | }); 400 | }); 401 | }); 402 | }); 403 | 404 | context('mozTelephony', function () { 405 | context('safe', function () { 406 | context(null, function () { 407 | var good = 'var a = "window.navigator.mozTelephony;";'; 408 | it(good, function () { 409 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 410 | }); 411 | }); 412 | }); 413 | context('dangerous', function () { 414 | context(null, function () { 415 | var bad = 'var phone = window.navigator.mozTelephony;'; 416 | it(bad, function () { 417 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 418 | }); 419 | }); 420 | }); 421 | }); 422 | 423 | context('mozTime', function () { 424 | context('safe', function () { 425 | context(null, function () { 426 | var good = 'var a = "mozTime;";'; 427 | it(good, function () { 428 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 429 | }); 430 | }); 431 | }); 432 | context('dangerous', function () { 433 | context(null, function () { 434 | var bad = 'var time = window.navigator.mozTime;'; 435 | it(bad, function () { 436 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 437 | }); 438 | }); 439 | }); 440 | }); 441 | 442 | context('mozVoicemail', function () { 443 | context('safe', function () { 444 | context(null, function () { 445 | var good = 'var a = "mozVoicemail";'; 446 | it(good, function () { 447 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 448 | }); 449 | }); 450 | }); 451 | context('dangerous', function () { 452 | context(null, function () { 453 | var bad = 'var voicemail = window.navigator.mozVoicemail;'; 454 | it(bad, function () { 455 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 456 | }); 457 | }); 458 | context(null, function () { 459 | var bad = 'var status = window.navigator.mozVoicemail.getStatus();'; 460 | it(bad, function () { 461 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 462 | }); 463 | }); 464 | }); 465 | }); 466 | 467 | context('mozapp', function () { 468 | context('safe', function () { 469 | context(null, function () { 470 | var good = 'var a = "mozapp";'; 471 | it(good, function () { 472 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 473 | }); 474 | }); 475 | }); 476 | context('dangerous', function () { 477 | context(null, function () { 478 | var bad = 'var a = document.createElement("iframe"); a.mozapp = data.app; document.appendChild(a);'; 479 | it(bad, function () { 480 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 481 | }); 482 | }); 483 | context(null, function () { 484 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 485 | var bad = 'iframe.setAttribute("mozapp", data.app);'; 486 | it.skip(bad, function () { 487 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 488 | }); 489 | }); 490 | }); 491 | }); 492 | 493 | context('mozaudiochannel', function () { 494 | context('safe', function () { 495 | context(null, function () { 496 | var good = 'var a = "mozaudiochannel=content.toString()";'; 497 | it(good, function () { 498 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 499 | }); 500 | }); 501 | }); 502 | context('dangerous', function () { 503 | context(null, function () { 504 | var bad = 'var a = document.createElement("audio"); a.mozaudiochannel = "content"; document.appendChild(a);'; 505 | it(bad, function () { 506 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 507 | }); 508 | }); 509 | context(null, function () { 510 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 511 | var bad = 'var a = document.createElement("audio"); a.setAttribute("mozaudiochannel", data.app);'; 512 | it.skip(bad, function () { 513 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 514 | }); 515 | }); 516 | }); 517 | }); 518 | 519 | context('moznetworkdownload', function () { 520 | context('safe', function () { 521 | context(null, function () { 522 | var good = 'var a = "moznetworkdownload";'; 523 | it(good, function () { 524 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 525 | }); 526 | }); 527 | }); 528 | context('dangerous', function () { 529 | context(null, function () { 530 | var bad = 'addEventListener("moznetworkdownload", downloadHandler);'; 531 | it(bad, function () { 532 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 533 | }); 534 | }); 535 | }); 536 | }); 537 | 538 | context('moznetworkupload', function () { 539 | context('safe', function () { 540 | context(null, function () { 541 | var good = 'var a = "moznetworkupload";'; 542 | it(good, function () { 543 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 544 | }); 545 | }); 546 | }); 547 | context('dangerous', function () { 548 | context(null, function () { 549 | var bad = 'addEventListener("moznetworkupload", downloadHandler);'; 550 | it(bad, function () { 551 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 552 | }); 553 | }); 554 | }); 555 | }); 556 | 557 | context('mozWifiManager', function () { 558 | context('safe', function () { 559 | context(null, function () { 560 | var good = 'var a = "mozWifiManager";'; 561 | it(good, function () { 562 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 563 | }); 564 | }); 565 | context(null, function () { 566 | var good = 'somethingNotNavigator.mozWifiManager;'; 567 | it(good, function () { 568 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 569 | }); 570 | }); 571 | }); 572 | context('dangerous', function () { 573 | context(null, function () { 574 | var bad = 'var wifi = navigator.mozWifiManager;'; 575 | it(bad, function () { 576 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 577 | }); 578 | }); 579 | context(null, function () { 580 | var bad = 'var networks = navigator.mozWifiManager.getNetworks();'; 581 | it(bad, function () { 582 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 583 | }); 584 | }); 585 | context(null, function () { 586 | var bad = 'var wifi = navigator.mozWifiManager;'; 587 | it(bad, function () { 588 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 589 | }); 590 | }); 591 | }); 592 | }); 593 | 594 | }); //describe('Mozilla specific tests'... 595 | })(); 596 | -------------------------------------------------------------------------------- /tests/cases/newFunction.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | describe('new Function() tests', function() { 3 | context('ignores safe patterns', function() { 4 | context(null, function () { 5 | var good = 'var Function = "static string";'; 6 | it(good, function(){ 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function() { 12 | context(null, function () { 13 | var bad = 'new Function("alert(0)")();'; 14 | it(bad, function(){ 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 20 | var bad = 'var a = Function; new a("alert(0)")();'; 21 | it.skip(bad, function(){ 22 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 23 | }); 24 | }); 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /tests/cases/outerHTML.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('outerHTML tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'good.outerHTML = "static string";'; 6 | it.skip(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = document.createElement("div"); a.setAttribute("outerHTML", "

bad

"); document.body.appendChild(a);'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'var getInnerHtml = document.getElementById("node").outerHTML;'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | context(null, function () { 23 | var good = '//div.outerHTML = this is a comment'; 24 | it(good, function () { 25 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 26 | }); 27 | }); 28 | }); 29 | context('detects dangerous patterns', function () { 30 | context(null, function () { 31 | var bad = 'dangerous.outerHTML=document.location;'; 32 | it(bad, function () { 33 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 34 | }); 35 | }); 36 | context(null, function () { 37 | var bad = 'div.outerHTML = "static string" + someVariable;'; 38 | it(bad, function () { 39 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 40 | }); 41 | }); 42 | }); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /tests/cases/parseFromString.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('parseFromString tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "parseFromString";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'doc = parser.parseFromString("

somehtml

", "text/html");'; 12 | it.skip(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | }); 17 | context('detects dangerous patterns', function () { 18 | context(null, function () { 19 | var bad = 'doc = parser.parseFromString(someVar, "text/html");'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /tests/cases/production_ruletests.js: -------------------------------------------------------------------------------- 1 | describe('Testing production rules (common/rules.json)', function () { 2 | $.ajax({ 3 | url: '../common/rules.json', 4 | async: false, 5 | dataType: 'json' 6 | }).done(function (ruleData) { 7 | 8 | ruleData.forEach(function (rule) { 9 | describe('Rule: ' + rule.name, function () { 10 | 11 | rule.testhit.split(";").forEach(function (testsplit) { 12 | if(testsplit.trim()!=""){ 13 | it(rule.source + " should match " + testsplit, function () { 14 | ScanJS.loadRules([rule]); 15 | var results = ScanJS.scan(testsplit); 16 | chai.expect(results.length).to.equal(1); 17 | }); 18 | } 19 | }); 20 | 21 | it(rule.name + " should not match " + rule.testmiss, function () { 22 | ScanJS.loadRules([rule]); 23 | var results = ScanJS.scan(rule.testmiss); 24 | chai.expect(results).to.have.length(0); 25 | }); 26 | }); 27 | }); 28 | }); 29 | }) -------------------------------------------------------------------------------- /tests/cases/sessionStorage.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('sessionStorage tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "sessionStorage";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function () { 12 | context(null, function () { 13 | var bad = 'window.sessionStorage.setItem("username", "John");'; 14 | it(bad, function () { 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | var bad = 'sessionStorage.setItem("username", "John");'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'var a = sessionStorage; a.setItem("username", "John");'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /tests/cases/setInterval.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('setInterval tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "window.setInterval";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function () { 12 | context(null, function () { 13 | var bad = 'setInterval("console.log(1)", 500);'; 14 | it(bad, function () { 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | var bad = 'var intervalID = window.setInterval("console.log(2)", 500);'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'something.setInterval("console.log(3)", 500);'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 32 | var bad = 'var a = window.setInterval; a("console.log(4)", 300);'; 33 | it.skip(bad, function () { 34 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 35 | }); 36 | }); 37 | context(null, function () { 38 | // issue 82 - https://github.com/mozilla/scanjs/issues/82 39 | var bad = 'var a = "setInterval"; window[a]("console.log(5)", 300);'; 40 | it.skip(bad, function () { 41 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /tests/cases/setTimeout.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('setTimeout tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "window.setTimeout";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | }); 11 | context('detects dangerous patterns', function () { 12 | context(null, function () { 13 | var bad = 'setTimeout("console.log(1)", 500);'; 14 | it(bad, function () { 15 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 16 | }); 17 | }); 18 | context(null, function () { 19 | var bad = 'var intervalID = window.setTimeout("console.log(2)", 500);'; 20 | it(bad, function () { 21 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 22 | }); 23 | }); 24 | context(null, function () { 25 | var bad = 'something.setTimeout("console.log(3)", 500);'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 32 | var bad = 'var a = window.setTimeout; a("console.log(4)", 300);'; 33 | it.skip(bad, function () { 34 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 35 | }); 36 | }); 37 | context(null, function () { 38 | // issue 82 - https://github.com/mozilla/scanjs/issues/82 39 | var bad = 'var a = "setTimeout"; window[a]("console.log(5)", 300);'; 40 | it.skip(bad, function () { 41 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /tests/cases/src.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('src attribute tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var a = "something.src";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var src = "something";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'var src = img.src;'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | context(null, function () { 23 | var good = 'var a = document.createElement("script"); a.src = "static string"; document.body.appendChild(a);'; 24 | it.skip(good, function () { 25 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 26 | }); 27 | }); 28 | }); 29 | context('detects dangerous patterns', function () { 30 | context(null, function () { 31 | var bad = 'obj.src = "mystring";'; 32 | it(bad, function () { 33 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 34 | }); 35 | }); 36 | context(null, function () { 37 | var bad = 'var a = document.createElement("script"); a.src = variable; document.body.appendChild(a);'; 38 | it(bad, function () { 39 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 40 | }); 41 | }); 42 | }); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /tests/cases/test_ruletests.js: -------------------------------------------------------------------------------- 1 | describe('Testing rule templates (common/template_rules.json)', function () { 2 | $.ajax({ 3 | url: '../common/template_rules.json', 4 | async: false, 5 | dataType: 'json' 6 | }).done(function (ruleData) { 7 | 8 | ruleData.forEach(function (rule) { 9 | describe('Rule: ' + rule.name, function () { 10 | it(rule.name + " should match template " + rule.name, function () { 11 | var template=ScanJS.parseRule(rule); 12 | chai.expect(template).to.equal(rule.name); 13 | }); 14 | 15 | rule.testhit.split(";").forEach(function (testsplit) { 16 | if(testsplit.trim()!=""){ 17 | it(rule.source + " should match " + testsplit, function () { 18 | ScanJS.loadRules([rule]); 19 | var results = ScanJS.scan(testsplit); 20 | chai.expect(results.length).to.equal(1); 21 | }); 22 | } 23 | }); 24 | 25 | it(rule.name + " should not match " + rule.testmiss, function () { 26 | ScanJS.loadRules([rule]); 27 | var results = ScanJS.scan(rule.testmiss); 28 | chai.expect(results).to.have.length(0); 29 | }); 30 | }); 31 | }); 32 | }); 33 | }) -------------------------------------------------------------------------------- /tests/cases/window.open.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | describe('window.open tests', function () { 3 | context('ignores safe patterns', function () { 4 | context(null, function () { 5 | var good = 'var open = "string window.open";'; 6 | it(good, function () { 7 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = {}; a.open = "string"; '; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'var a = {}; a.open = function () { alert(1); }; a.open("1", "2", a);'; 18 | it(good, function () { 19 | chai.expect(ScanJS.scan(good, document.location.pathname)).to.be.empty; 20 | }); 21 | }); 22 | }); 23 | context('detects dangerous patterns', function () { 24 | context(null, function () { 25 | var bad = 'win = window.open("http://www.mozilla.org", "name", fets);'; 26 | it(bad, function () { 27 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 28 | }); 29 | }); 30 | context(null, function () { 31 | // issue 76 - https://github.com/mozilla/scanjs/issues/76 32 | var bad = 'var o = window.open; o("http://www.mozilla.org", "name", {});'; 33 | it.skip(bad, function () { 34 | chai.expect(ScanJS.scan(bad, document.location.pathname)).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /tests/css/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scan.js Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
More detailed tests (work in progress)
22 |

Scan.js Mocha Unit Tests

23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/js/loadrules.js: -------------------------------------------------------------------------------- 1 | ScanJS.loadRulesFile("../common/rules.json"); 2 | -------------------------------------------------------------------------------- /tests/js/main.js: -------------------------------------------------------------------------------- 1 | mocha.setup('bdd'); 2 | $(document).ready(function() { mocha.run(); }); --------------------------------------------------------------------------------