├── .gitignore ├── tests ├── js │ ├── loadrules.js │ └── main.js ├── mocha-includes.js ├── cases │ ├── escapeHTML.js │ ├── indexedDB.js │ ├── parseFromString.js │ ├── newFunction.js │ ├── createContextualFragment.js │ ├── CustomEvent.js │ ├── eval.js │ ├── geolocation.js │ ├── addIdleObserver.js │ ├── sessionStorage.js │ ├── localStorage.js │ ├── production_ruletests.js │ ├── getDeviceStorage.js │ ├── crypto.generateCRMFRequest.js │ ├── message.js │ ├── window.open.js │ ├── data.js │ ├── test_ruletests.js │ ├── document.write.js │ ├── action.js │ ├── document.writeln.js │ ├── addEventListener.js │ ├── href.js │ ├── src.js │ ├── outerHTML.js │ ├── setTimeout.js │ ├── setInterval.js │ ├── placeholders.js │ ├── innerhtml.js │ └── moz │ │ └── moz.js ├── index.html ├── TESTING ├── advanced.html └── css │ └── mocha.css ├── client ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── img │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── rules.readme.md ├── js │ ├── locationctrl.js │ ├── lib │ │ ├── utils.js │ │ ├── angular-route-1.2.13.min.js │ │ ├── localforage.min.js │ │ ├── walk.js │ │ └── acorn_loose.js │ ├── main.js │ ├── scanworker.js │ ├── rulesctrl.js │ ├── scanservice.js │ ├── experimentctrl.js │ └── scanctrl.js ├── css │ ├── angular-csp.css │ ├── prettify.css │ ├── dashboard.css │ ├── app.css │ ├── codemirror-mdn.css │ └── codemirror.css ├── partials │ ├── rules.html │ ├── experiment.html │ └── scan.html └── index.html ├── LICENSE ├── stackato.yml ├── deploy-ghpages.sh ├── server.js ├── package.json ├── scanner.js ├── common ├── template_rules.json └── scan.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /nbproject/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /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(); }); -------------------------------------------------------------------------------- /client/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /client/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/scanjs/HEAD/client/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/js/locationctrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | scanjsModule.controller('LocationCtrl', ['$scope', '$location', function LocationCtrl($scope, $location) { 4 | $scope.tabBtnClass = function (page) { 5 | var current = $location.path().substring(1) || 'scan'; 6 | //console.log("l", $location.hash(), "ls", $location.hash().substring(2)) 7 | return page === current ? 'active' : ''; 8 | } 9 | }]); -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /tests/mocha-includes.js: -------------------------------------------------------------------------------- 1 | global.acorn = require("acorn"); 2 | global.chai = require("chai"); 3 | global.assert = chai.assert; 4 | global.should = require("chai").should(); 5 | global.expect = require("chai").expect; 6 | global.AssertionError = require("chai").AssertionError; 7 | 8 | global.ScanJS = require("../common/scan.js"); 9 | var fs = require("fs"); 10 | var signatures = JSON.parse(fs.readFileSync("../common/rules.json", "utf8")); 11 | ScanJS.parser(acorn); 12 | ScanJS.loadRules(signatures); 13 | -------------------------------------------------------------------------------- /deploy-ghpages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This makes sure, that we don't deploy pull requests to gh-pages :-) 4 | if [ $TRAVIS_PULL_REQUEST != false ]; 5 | then 6 | echo "Not deploying test-run for a pull request" 7 | exit 0 8 | fi 9 | 10 | ( 11 | git init 12 | git config user.name "Travis-CI" 13 | git config user.email "travis@nodemeatspace.com" 14 | git add -v . 15 | git commit -v -m "Deployed to Github Pages" 16 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1 17 | ) 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var static = require("node-static"); 3 | var file = new static.Server('.', { 4 | headers: { 5 | "Content-Security-Policy": "default-src 'self'; object-src 'none'; img-src 'self' data:; script-src 'self' 'unsafe-eval'", 6 | } 7 | }); 8 | 9 | const PORT = process.env.PORT || 4000; 10 | require ('http').createServer(function (req, res) { 11 | req.addListener('end', function () { 12 | file.serve(req, res); 13 | }).resume(); 14 | }).listen(PORT); 15 | 16 | console.log("> node-static is listening on http://127.0.0.1:"+PORT); 17 | -------------------------------------------------------------------------------- /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/js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var scanjsModule = angular.module('scanjs', ['ui.bootstrap', 'ngRoute']); 4 | 5 | scanjsModule.config(['$routeProvider', 6 | function($routeProvider) { 7 | $routeProvider. 8 | when('/scan', { 9 | templateUrl: 'partials/scan.html', 10 | controller: 'ScanCtrl' 11 | }). 12 | when('/rules', { 13 | templateUrl: 'partials/rules.html', 14 | controller: 'RuleListCtrl' 15 | }). 16 | when('/experiment', { 17 | templateUrl: 'partials/experiment.html', 18 | controller: 'ExperimentCtrl' 19 | }). 20 | otherwise({ 21 | redirectTo: '/scan' 22 | }); 23 | }]); -------------------------------------------------------------------------------- /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, "/tests/")).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, "/tests/")).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, "/tests/")).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 23 | }); 24 | }); 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(bad, {locations:true}), "/tests/")).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(acorn.parse(bad, {locations:true}), "/tests/")).not.to.be.empty; 23 | }); 24 | }); 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(bad, {locations:true}), "/tests/")).not.to.be.empty; 22 | }); 23 | }); 24 | }); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /client/js/scanworker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* this makes sure that console.log can be used, even if it is undefined. 4 | We won't see the message though, since this kind of postMessage isn't handled in scanservice.js */ 5 | if (typeof console === "undefined") { 6 | console = {}; 7 | console.log = function consoleShim(mesg) { 8 | postMessage({'type':'log', 'message': mesg}); 9 | } 10 | } 11 | 12 | importScripts('lib/acorn.js', 13 | 'lib/walk.js', 14 | 'lib/acorn_loose.js', 15 | '../../common/scan.js'); 16 | 17 | //load default rules 18 | //ScanJS.loadRulesFile("../../common/rules.json") 19 | 20 | onmessage = function (evt) { 21 | if (evt.data.call === 'scan') { 22 | var args = evt.data.arguments; 23 | var source = args[0]; 24 | var rules; 25 | 26 | var file = args[1]; 27 | 28 | var ast = acorn.parse(source, {locations: true}); 29 | 30 | var findings = ScanJS.scan(ast,file); 31 | postMessage({"filename": file, "findings": findings}); 32 | } 33 | else if(evt.data.call === 'updateRules'){ 34 | console.log('scanworker.js: Loaded '+evt.data.rules.length+" rules.") 35 | ScanJS.loadRules(evt.data.rules) 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good.CustomEvent = "CustomEvent";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations:true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'addIdleObserver = "static string";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(bad, {locations:true}), "/tests/")).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /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 | 35 | 36 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 28 | }); 29 | }); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function() { 11 | var good = 'var localStorage = "asdf";'; 12 | it(good, function(){ 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /tests/cases/production_ruletests.js: -------------------------------------------------------------------------------- 1 | describe('Testing production rules (common/rules.json)', function () { 2 | function testSetup(ruleData) { 3 | 4 | ruleData.forEach(function (rule) { 5 | describe('Rule: ' + rule.name, function () { 6 | 7 | rule.testhit.split(";").forEach(function (testsplit) { 8 | if (testsplit.trim() != "") { 9 | it(rule.source + " should match " + testsplit, function () { 10 | ScanJS.loadRules([rule]); 11 | var ast = acorn.parse(testsplit, {locations: true}); 12 | var results = ScanJS.scan(ast); 13 | chai.expect(results.length).to.equal(1); 14 | }); 15 | } 16 | }); 17 | 18 | it(rule.name + " should not match " + rule.testmiss, function () { 19 | ScanJS.loadRules([rule]); 20 | var ast = acorn.parse(rule.testmiss, {locations: true}); 21 | var results = ScanJS.scan(ast); 22 | chai.expect(results).to.have.length(0); 23 | }); 24 | }); 25 | }); 26 | } 27 | if (typeof $ !== "undefined") { 28 | $.ajax({ 29 | url: '../common/rules.json', 30 | async: false, 31 | dataType: 'json' 32 | }).done(testSetup); 33 | } 34 | else if (typeof require !== "undefined") { 35 | var fs = require("fs"); 36 | var signatures = JSON.parse(fs.readFileSync("../common/rules.json", "utf8")); 37 | testSetup(signatures); 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /client/js/rulesctrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | scanjsModule.controller('RuleListCtrl', ['$scope', 'ScanSvc', function RuleListCtrl($scope, ScanSvc) { 4 | $scope.rulesFile = "../common/rules.json"; 5 | $scope.rules = []; //JSON rules object 6 | 7 | document.getElementById("rule-file-input").addEventListener("change", function (evt) { 8 | $scope.handleFileUpload(this.files); 9 | }); 10 | 11 | function loadRulesFile(rulesFile) { 12 | //TODO rewrite with $http() 13 | var request = new XMLHttpRequest(); 14 | request.open('GET', rulesFile); 15 | 16 | request.onload = function () { 17 | if (request.status >= 200 && request.status < 400) { 18 | $scope.rules = JSON.parse(request.responseText); 19 | 20 | ScanSvc.loadRules($scope.rules); 21 | } else { 22 | console.log('Error loading ' + rules) 23 | } 24 | $scope.$apply(); 25 | }; 26 | 27 | request.onerror = function () { 28 | console.log('Connection error while loading ' + rulesFile) 29 | }; 30 | request.send(); 31 | } 32 | 33 | $scope.handleFileUpload = function handleFileUpload(fileList) { 34 | 35 | $scope.rulesFile = fileList[0].name; 36 | 37 | var reader = new FileReader(); 38 | reader.onload = function () { 39 | $scope.rules = JSON.parse(this.result); 40 | ScanSvc.loadRules($scope.rules); 41 | $scope.$apply(); 42 | } 43 | 44 | reader.readAsText(fileList[0]) 45 | }; 46 | 47 | loadRulesFile($scope.rulesFile); 48 | }]); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scanjs", 3 | "version": "0.0.2", 4 | "description": "Static analysis tool for javascript codebases", 5 | "main": "scanner.js", 6 | "bin": { 7 | "scanjs": "./scanner.js", 8 | "scanjs-server": "./server.js" 9 | }, 10 | "preferGlobal": true, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mozilla/scanjs.git" 14 | }, 15 | "keywords": [ 16 | "scanjs", 17 | "code-analysis", 18 | "javascript", 19 | "static-analysis" 20 | ], 21 | "author": "Paul Theriault, Frederik Braun, Rob Fletcher", 22 | "license": "MPL", 23 | "dependencies": { 24 | "acorn": "~0.3.0", 25 | "tern": "~0.5.0", 26 | "optimist": "~0.5.2", 27 | "jszip": "~2.2.0", 28 | "node-static": "~0.7.3", 29 | "js-beautify":"~1.4.2" 30 | }, 31 | "devDependencies": { 32 | "mocha": "*", 33 | "chai": "*" 34 | }, 35 | "scripts": { 36 | "test": "cd tests && mocha -r mocha-includes cases/placeholders.js cases/test_ruletests.js cases/production_ruletests.js && mocha -r mocha-includes cases/innerhtml.js cases/CustomEvent.js cases/addIdleObserver.js cases/createContextualFragment.js cases/crypto.generateCRMFRequest.js cases/document.write.js cases/document.writeln.js cases/eval.js cases/geolocation.js cases/href.js cases/indexedDB.js cases/localStorage.js cases/message.js cases/newFunction.js cases/outerHTML.js cases/parseFromString.js cases/sessionStorage.js cases/setInterval.js cases/setTimeout.js cases/src.js cases/getDeviceStorage.js cases/moz/moz.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = "generateCRMFRequest";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 29 | }); 30 | }); 31 | }); 32 | }); 33 | })(); 34 | -------------------------------------------------------------------------------- /client/partials/rules.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |

Load new rules

8 | Load a JSON rules file to change rules: 9 | 10 |

Current rules

11 | Number of Rules: {{rules.length}} 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 |
Rule NameRule DescriptionRule Definition
{{rule.name}}{{rule.desc}} 27 |
{{rule.source}}
28 |
32 |
33 |
34 |
35 |
36 |
-------------------------------------------------------------------------------- /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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(good, {locations:true}), "/tests/")).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(acorn.parse(bad, {locations:true}), "/tests/")).not.to.be.empty; 34 | }); 35 | }); 36 | }); 37 | }); 38 | })(); 39 | -------------------------------------------------------------------------------- /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, "/tests/")).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, "/tests/")).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, "/tests/")).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, "/tests/")).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, "/tests/")).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /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, "/tests/")).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, "/tests/")).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, "/tests/")).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, "/tests/")).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, "/tests/")).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, "/tests/")).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 | function testSetup(ruleData) { 3 | 4 | ruleData.forEach(function (rule) { 5 | describe('Rule: ' + rule.name, function () { 6 | it(rule.name + " should match template " + rule.name, function () { 7 | var template = ScanJS.parseRule(rule); 8 | chai.expect(rule.name).to.equal(template); 9 | }); 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 ast = acorn.parse(testsplit, {locations: true}); 16 | var results = ScanJS.scan(ast); 17 | chai.expect(results.length).to.equal(1); 18 | }); 19 | } 20 | }); 21 | 22 | it(rule.name + " should not match " + rule.testmiss, function () { 23 | ScanJS.loadRules([rule]); 24 | var ast = acorn.parse(rule.testmiss, {locations: true}); 25 | var results = ScanJS.scan(ast); 26 | chai.expect(results).to.have.length(0); 27 | }); 28 | }); 29 | }); 30 | } 31 | if (typeof $ !== "undefined") { 32 | $.ajax({ 33 | url: '../common/template_rules.json', 34 | async: false, 35 | dataType: 'json' 36 | }).done(testSetup); 37 | } 38 | else if (typeof require !== "undefined") { 39 | var fs = require("fs"); 40 | var signatures = JSON.parse(fs.readFileSync("../common/template_rules.json", "utf8")); 41 | testSetup(signatures); 42 | } 43 | }) -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good = "document.write";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 30 | }); 31 | }); 32 | }); 33 | }); 34 | })(); 35 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'good = "document.writeln";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 35 | }); 36 | }); 37 | }); 38 | }); 39 | })(); 40 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 34 | }); 35 | }); 36 | }); 37 | }); 38 | })(); 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var a = document.createElement("a"); a.setAttribute("href", "http://mozilla.org"); document.body.appendChild(a);'; 12 | it(good, function(){ 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 14 | }); 15 | }); 16 | context(null, function () { 17 | var good = 'var a = document.createElement("a"); a.setAttribute("href", 1); document.body.appendChild(a);'; 18 | it(good, function(){ 19 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 20 | }); 21 | }); 22 | }); 23 | context('detects dangerous patterns', function() { 24 | /* deactivated, failing test. 25 | context(null, function () { 26 | var bad = 'a.href ="javascript:alert(0);";'; 27 | it(bad, function(){ 28 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 29 | }); 30 | }); 31 | */ 32 | context(null, function () { 33 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 34 | var bad = 'var a = document.createElement("a"); a.setAttribute("href", "javascript:alert(0)"); document.body.appendChild(a);'; 35 | it.skip(bad, function(){ 36 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 37 | }); 38 | }); 39 | }); 40 | }); 41 | })(); 42 | -------------------------------------------------------------------------------- /tests/TESTING: -------------------------------------------------------------------------------- 1 | TESTING 2 | ====== 3 | 4 | To run tests, just use one of the following commands. 5 | 6 | Equivalent to visiting test/index.html in the browser: 7 | mocha -r mocha-includes cases/placeholders.js cases/test_ruletests.js cases/production_ruletests.js 8 | 9 | Equivalent to visiting test/advanced.html in the browser: 10 | mocha -r mocha-includes cases/innerhtml.js cases/CustomEvent.js cases/addIdleObserver.js cases/createContextualFragment.js cases/crypto.generateCRMFRequest.js cases/document.write.js cases/document.writeln.js cases/eval.js cases/geolocation.js cases/href.js cases/indexedDB.js cases/localStorage.js cases/message.js cases/newFunction.js cases/outerHTML.js cases/parseFromString.js cases/sessionStorage.js cases/setInterval.js cases/setTimeout.js cases/src.js cases/getDeviceStorage.js cases/moz/moz.js 11 | 12 | See disbaled tests in JS format below 13 | 14 | List of tests in JS format: 15 | -------------------------------------------------------------------------------- 16 | 17 | 18 | var disabledTests = ["action.js", 19 | "addEventListener.js", 20 | "data.js", 21 | "escapeHTML.js", 22 | "window.open.js"] 23 | 24 | 25 | var standardTests = ["placeholders.js", 26 | "test_ruletests.js", 27 | "production_ruletests.js"] 28 | 29 | var advancedTests = ["innerhtml.js", 30 | "CustomEvent.js", 31 | "addIdleObserver.js", 32 | "createContextualFragment.js", 33 | "crypto.generateCRMFRequest.js", 34 | "document.write.js", 35 | "document.writeln.js", 36 | "eval.js", 37 | "geolocation.js", 38 | "href.js", 39 | "indexedDB.js", 40 | "localStorage.js", 41 | "message.js", 42 | "newFunction.js", 43 | "outerHTML.js", 44 | "parseFromString.js", 45 | "sessionStorage.js", 46 | "setInterval.js", 47 | "setTimeout.js", 48 | "src.js", 49 | "getDeviceStorage.js", 50 | "moz/moz.js"] 51 | 52 | var allTests = disabledTests+standardTests+advancedTests; 53 | 54 | 55 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 8 | }); 9 | }); 10 | context(null, function () { 11 | var good = 'var src = "something";'; 12 | it(good, function () { 13 | chai.expect(ScanJS.scan(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).to.be.empty; 26 | }); 27 | }); 28 | }); 29 | context('detects dangerous patterns', function () { 30 | /* deactivated, failing test. 31 | context(null, function () { 32 | var bad = 'obj.src = "mystring";'; 33 | it(bad, function () { 34 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 35 | }); 36 | });*/ 37 | context(null, function () { 38 | var bad = 'var a = document.createElement("script"); a.src = variable; document.body.appendChild(a);'; 39 | it(bad, function () { 40 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 41 | }); 42 | }); 43 | }); 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 40 | }); 41 | }); 42 | }); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /client/js/scanservice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | scanjsModule.factory('ScanSvc', ['$rootScope', '$http', function($rootScope, $http) { 4 | var ScanService = { 5 | //results:[], 6 | ready:false, 7 | rules:null, 8 | init:function(rules){ 9 | this.rules=rules; 10 | this.ready=true; 11 | }, 12 | newScan: function(file,source) { 13 | var fileName = file || 'inline'; 14 | this.scanWorker.postMessage({call: 'scan', arguments: [source, fileName]}); 15 | }, 16 | addResults: function(results) { 17 | $rootScope.$broadcast('NewResults', results); 18 | }, 19 | loadRules:function(ruleData){ 20 | this.rules=ruleData; 21 | this.scanWorker.postMessage({call: 'updateRules', rules: ruleData}); 22 | } 23 | }; 24 | ScanService.scanWorker = new Worker("js/scanworker.js"); 25 | ScanService.scanWorker.addEventListener("message", function (evt) { 26 | if (('findings' in evt.data) && ('filename' in evt.data)) { 27 | if (evt.data.findings.length > 0) { 28 | if (evt.data.findings[0].type == 'error') { 29 | $rootScope.$broadcast('ScanError', evt.data.findings[0]) 30 | return; 31 | } 32 | } 33 | ScanService.addResults(evt.data); 34 | } 35 | else if ('error' in evt.data) { 36 | // This is for errors in the worker, not in the scanning. 37 | // Exceptions (like SyntaxErrors) when scanning files 38 | // are in the findings. 39 | var exception = evt.data.error; 40 | if (e instanceof SyntaxError) { 41 | $rootScope.$broadcast('ScanError', {filename: evt.data.filename, name: exception.name, loc: exception.loc, message: exception.message }) 42 | } else { 43 | throw e; // keep throwing unexpected things. 44 | } 45 | } 46 | }); 47 | 48 | ScanService.scanWorker.onerror = function (e) { console.log('ScanWorker Error: ', e) }; 49 | 50 | $http({method: 'GET', url: "../common/rules.json"}). 51 | success(function(data, status, headers, config) { 52 | ScanService.loadRules(data); 53 | }); 54 | 55 | return ScanService; 56 | }]); 57 | -------------------------------------------------------------------------------- /client/js/experimentctrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | scanjsModule.controller('ExperimentCtrl', ['$scope', 'ScanSvc', function ExperimentCtrl($scope, ScanSvc) { 4 | if (!document.getElementById("experiment-mirror").children.length) { 5 | $scope.codeMirror = new CodeMirror(document.getElementById('experiment-mirror'), { 6 | mode: 'javascript', 7 | lineNumbers: true, 8 | theme: 'mdn-like', 9 | value: "bar.foo\nfoo=something\nbaz.bar=stuff\nfoo(something)\nfoo.bar\nfoo.bar()\neval(test)\nfoo.innerHTML=danger", 10 | tabsize: 2, 11 | styleActiveLine: true 12 | }); 13 | } 14 | $scope.results=[]; 15 | $scope.ready=false; 16 | $scope.rule="eval()" 17 | 18 | var ruleData={ 19 | "name": "manual rule", 20 | "source": $scope.rule, 21 | "testhit": $scope.rule, 22 | "testmiss": "", 23 | "desc": "Manual input.", 24 | "threat": "example" 25 | }; 26 | 27 | $scope.runScan = function () { 28 | $scope.results = []; 29 | $scope.error = null; 30 | ScanJS.loadRules(ScanSvc.rules); 31 | var code = $scope.codeMirror.getValue(); 32 | try { 33 | var ast = acorn.parse(code, { locations: true }); 34 | $scope.results = ScanJS.scan(ast); 35 | } catch(e) { 36 | $scope.error = e; 37 | console.error(e); 38 | } 39 | $scope.lastScan = $scope.runScan; 40 | }; 41 | 42 | $scope.runManualScan = function () { 43 | ruleData.source = $scope.rule; 44 | ScanJS.loadRules([ruleData]); 45 | 46 | $scope.results = []; 47 | $scope.error = null; 48 | var code = $scope.codeMirror.getValue(); 49 | try { 50 | var ast = acorn.parse(code, { locations: true }); 51 | $scope.results = ScanJS.scan(ast); 52 | //put ast on global variable for debugging purposes. 53 | window.ast = ast; 54 | } catch(e) { 55 | $scope.error = e; 56 | console.error(e); 57 | } 58 | $scope.lastScan = $scope.runManualScan; 59 | }; 60 | 61 | $scope.showResult = function (filename,line, col) { 62 | $scope.codeMirror.setCursor(line - 1, col || 0); 63 | $scope.codeMirror.focus(); 64 | }; 65 | 66 | $scope.add_placeholder_char = function() { 67 | $scope.rule += '$_any'; 68 | } 69 | $scope.lastScan=$scope.runScan; 70 | 71 | }]); 72 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ScanJS 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 | 37 | 38 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tests/cases/placeholders.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | describe('Testing placeholders $_any, $_contains etc', function () { 5 | context('$_any', function () { 6 | 7 | it(" $_any.foo should match something.foo", function () { 8 | ScanJS.loadRules([{name:"$_any.foo",source:"$_any.foo"}]); 9 | var results = ScanJS.scan(acorn.parse("something.foo;", {locations: true})); 10 | chai.expect(results).to.have.length(1); 11 | }); 12 | 13 | it(" $_any.foo should not match something.bar", function () { 14 | ScanJS.loadRules([{name:"$_any.foo",source:"$_any.foo"}]); 15 | var results = ScanJS.scan(acorn.parse("something.bar;", {locations:true})); 16 | chai.expect(results).to.have.length(0); 17 | }); 18 | 19 | it(" foo.$_any should match foo.something", function () { 20 | ScanJS.loadRules([{name:"foo.$_any",source:"foo.something"}]); 21 | var results = ScanJS.scan(acorn.parse("foo.something;", {locations:true})); 22 | chai.expect(results).to.have.length(1); 23 | }); 24 | 25 | it(" foo.$_any should not match bar.something", function () { 26 | ScanJS.loadRules([{name:"$_any.foo",source:"$_any.foo"}]); 27 | var results = ScanJS.scan(acorn.parse("something.bar;", {locations:true})); 28 | chai.expect(results).to.have.length(0); 29 | }); 30 | 31 | 32 | }); 33 | 34 | context('$_contains', function () { 35 | 36 | var rule= {name:"foo=$_unsafe",source:"foo=$_unsafe"}; 37 | 38 | it(rule.source + " match template callargs", function () { 39 | var template=ScanJS.parseRule(rule); 40 | chai.expect("assignment").to.equal(template); 41 | }); 42 | 43 | it(rule.source + " should match foo=identifier", function () { 44 | ScanJS.loadRules([rule]); 45 | var results = ScanJS.scan(acorn.parse("foo=identifier;", {locations:true})); 46 | chai.expect(results).to.have.length(1); 47 | }); 48 | 49 | it(rule.source + " should not match bar=identifier", function () { 50 | ScanJS.loadRules([rule]); 51 | var results = ScanJS.scan(acorn.parse("bar=identifier;", {locations:true})); 52 | chai.expect(results).to.have.length(0); 53 | }); 54 | 55 | it(rule.source + " should not match foo='literal'", function () { 56 | ScanJS.loadRules([rule]); 57 | var results = ScanJS.scan(acorn.parse("foo='literal;'", {locations:true})); 58 | chai.expect(results).to.have.length(0); 59 | }); 60 | }); 61 | }) 62 | -------------------------------------------------------------------------------- /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 | .fixed { 115 | position: fixed; 116 | min-width: 45%; 117 | } -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(good, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).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(acorn.parse(bad, {locations: true}), "/tests/")).not.to.be.empty; 53 | }); 54 | }); 55 | }); 56 | }); 57 | })(); 58 | -------------------------------------------------------------------------------- /client/partials/experiment.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |

23 | Results 24 |

25 |
26 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 50 | 51 |
LineRuleSourceDescription of Issue
Line {{result.line}} 46 | {{result.rule.name }}{{result.rule.source }}{{result.rule.desc}} 49 |
52 |
53 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scanner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*jshint node:true*/ 3 | 4 | // This script is for using scanjs server side 5 | 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var beautify = require('js-beautify').js_beautify; 9 | 10 | var parser = require(__dirname+ '/client/js/lib/acorn.js'); 11 | var ScanJS = require(__dirname + '/common/scan'); 12 | 13 | var signatures = JSON.parse(fs.readFileSync(__dirname + "/common/rules.json", "utf8")); 14 | 15 | ScanJS.parser(parser); 16 | ScanJS.loadRules(signatures); 17 | 18 | var argv = require('optimist').usage('Usage: $node scan.js -t [path/to/app] -o [resultFile.json]').demand(['t']).argv; 19 | 20 | var dive = function(dir, action) { 21 | if( typeof action !== 'function') { 22 | action = function(error, file) { 23 | console.log(">" + file) 24 | }; 25 | } 26 | list = fs.readdirSync(dir); 27 | list.forEach(function(file) { 28 | var fullpath = dir + '/' + file; 29 | try { 30 | var stat = fs.statSync(fullpath); 31 | } catch(e) { 32 | console.log("SKIPPING FILE: Could not stat " + fullpath); 33 | } 34 | if(stat && stat.isDirectory()) { 35 | dive(fullpath, action); 36 | } else { 37 | action(file, fullpath); 38 | } 39 | }); 40 | }; 41 | var writeReport = function(results, name) { 42 | if(fs.existsSync(name)) { 43 | console.log("Error:output file already exists (" + name + "). Supply a different name using: -o [filename]") 44 | } 45 | fs.writeFile(name, JSON.stringify(results), function(err) { 46 | if(err) { 47 | console.log(err); 48 | } else { 49 | console.log("The scan results were saved to " + name); 50 | } 51 | }); 52 | }; 53 | 54 | 55 | if( typeof process != 'undefined' && process.argv[2]) { 56 | results = {}; 57 | reportname = argv.o ? argv.o : 'scanresults'; 58 | reportdir = reportname + "_files"; 59 | if(fs.existsSync(reportname) || fs.existsSync(reportdir)) { 60 | console.log("Error:output file or dir already exists (" + reportname + "). Supply a different name using: -o [filename]") 61 | 62 | } 63 | else { 64 | fs.mkdirSync(reportdir); 65 | dive(argv.t, function(file, fullpath) { 66 | var ext = path.extname(file.toString()); 67 | 68 | if(ext == '.js') { 69 | var content = fs.readFileSync(fullpath, 'utf8'); 70 | //beautify source so result snippet is meaningful 71 | var content = beautify(content, { indent_size: 2 }); 72 | 73 | var ast = parser.parse(content, { locations: true }); 74 | 75 | var scanresult = ScanJS.scan(ast, fullpath); 76 | if (scanresult.type == 'error') { 77 | console.log("SKIPPING FILE: Error in "+ fullpath+", at Line "+ scanresult.error.loc.line +", Column "+scanresult.error.loc.column+ ": " + scanresult.error.message); 78 | } 79 | results[fullpath] = scanresult; 80 | } 81 | }); 82 | // Flatten report file to remove files with no findings and tests with no results (i.e. empty arr) 83 | // TODO: Don't even store empty unless --overly-verbose or so.. 84 | for (var testedFile in results) { 85 | for (var testCase in results[testedFile]) { 86 | if (results[testedFile][testCase].length == 0) { 87 | delete(results[testedFile][testCase]); 88 | } 89 | } 90 | if (Object.keys(results[testedFile]).length == 0) { 91 | delete(results[testedFile]); 92 | } 93 | } 94 | writeReport(results, reportname + '.JSON'); 95 | } 96 | } else { 97 | console.log('usage: $ node scan.js path/to/app ') 98 | } 99 | -------------------------------------------------------------------------------- /client/js/lib/angular-route-1.2.13.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.13 3 | (c) 2010-2014 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(h,e,A){'use strict';function u(w,q,k){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,n){function y(){l&&(l.$destroy(),l=null);g&&(k.leave(g),g=null)}function v(){var b=w.current&&w.current.locals;if(e.isDefined(b&&b.$template)){var b=a.$new(),f=w.current;g=n(b,function(d){k.enter(d,null,g||c,function(){!e.isDefined(t)||t&&!a.$eval(t)||q()});y()});l=f.scope=b;l.$emit("$viewContentLoaded");l.$eval(h)}else y()}var l,g,t=b.autoscroll,h=b.onload||""; 7 | a.$on("$routeChangeSuccess",v);v()}}}function z(e,h,k){return{restrict:"ECA",priority:-400,link:function(a,c){var b=k.current,f=b.locals;c.html(f.$template);var n=e(c.contents());b.controller&&(f.$scope=a,f=h(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));n(a)}}}h=e.module("ngRoute",["ng"]).provider("$route",function(){function h(a,c){return e.extend(new (e.extend(function(){},{prototype:a})),c)}function q(a, 8 | e){var b=e.caseInsensitiveMatch,f={originalPath:a,regexp:a},h=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,e,b,c){a="?"===c?c:null;c="*"===c?c:null;h.push({name:b,optional:!!a});e=e||"";return""+(a?"":e)+"(?:"+(a?e:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=RegExp("^"+a+"$",b?"i":"");return f}var k={};this.when=function(a,c){k[a]=e.extend({reloadOnSearch:!0},c,a&&q(a,c));if(a){var b="/"==a[a.length-1]?a.substr(0,a.length- 9 | 1):a+"/";k[b]=e.extend({redirectTo:a},q(b,c))}return this};this.otherwise=function(a){this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,c,b,f,n,q,v,l){function g(){var d=t(),m=r.current;if(d&&m&&d.$$route===m.$$route&&e.equals(d.pathParams,m.pathParams)&&!d.reloadOnSearch&&!x)m.params=d.params,e.copy(m.params,b),a.$broadcast("$routeUpdate",m);else if(d||m)x=!1,a.$broadcast("$routeChangeStart",d,m),(r.current= 10 | d)&&d.redirectTo&&(e.isString(d.redirectTo)?c.path(u(d.redirectTo,d.params)).search(d.params).replace():c.url(d.redirectTo(d.pathParams,c.path(),c.search())).replace()),f.when(d).then(function(){if(d){var a=e.extend({},d.resolve),c,b;e.forEach(a,function(d,c){a[c]=e.isString(d)?n.get(d):n.invoke(d)});e.isDefined(c=d.template)?e.isFunction(c)&&(c=c(d.params)):e.isDefined(b=d.templateUrl)&&(e.isFunction(b)&&(b=b(d.params)),b=l.getTrustedResourceUrl(b),e.isDefined(b)&&(d.loadedTemplateUrl=b,c=q.get(b, 11 | {cache:v}).then(function(a){return a.data})));e.isDefined(c)&&(a.$template=c);return f.all(a)}}).then(function(c){d==r.current&&(d&&(d.locals=c,e.copy(d.params,b)),a.$broadcast("$routeChangeSuccess",d,m))},function(c){d==r.current&&a.$broadcast("$routeChangeError",d,m,c)})}function t(){var a,b;e.forEach(k,function(f,k){var p;if(p=!b){var s=c.path();p=f.keys;var l={};if(f.regexp)if(s=f.regexp.exec(s)){for(var g=1,q=s.length;g 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/partials/scan.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 39 | 40 |
41 | 42 |
43 |
44 |

Choose files to scan

45 |

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

46 |

47 | 48 | 72 |
73 |
74 | 75 | 121 | 122 |
123 |
124 |
125 |

File Viewer

126 |
127 |
128 |
129 | 130 |
-------------------------------------------------------------------------------- /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/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/scanctrl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | scanjsModule.controller('ScanCtrl', ['$scope', 'ScanSvc', function ScanCtrl($scope, ScanSvc) { 4 | if (!document.getElementById("codeMirrorDiv").children.length) { 5 | $scope.codeMirror = new CodeMirror(document.getElementById('codeMirrorDiv'), { 6 | mode: 'javascript', 7 | lineNumbers: true, 8 | theme: 'mdn-like', 9 | value: "", 10 | readOnly:true, 11 | tabsize: 2, 12 | styleActiveLine: true 13 | }); 14 | } 15 | $scope.codeMirrorManual = undefined; 16 | $scope.inputFiles = []; 17 | $scope.results=[]; 18 | $scope.errors=[]; 19 | $scope.filteredResults=[]; 20 | $scope.inputFilename=""; 21 | $scope.issueList=[]; 22 | $scope.throbInput = false; 23 | $scope.throbOutput = false; 24 | 25 | var pending = 0; 26 | var selectedFile = 0; 27 | var codeMirror_index = 0; 28 | 29 | document.getElementById("scan-file-input").addEventListener("change", function(evt) { 30 | $scope.handleFileUpload(this.files); 31 | }); 32 | 33 | $scope.run = function (source, filename) { 34 | //empty last scan 35 | $scope.results=[]; 36 | $scope.errors=[]; 37 | $scope.inputFiles.forEach(function (scriptFile, i) { 38 | if (document.getElementById('doScan_'+i).checked) { 39 | pending++; 40 | $scope.throbOutput = true; 41 | ScanSvc.newScan(scriptFile.name,scriptFile.asText()); 42 | } 43 | }); 44 | 45 | //update UI 46 | document.querySelector("#scan-input").classList.toggle("hidden",true); 47 | document.querySelector("#scan-results").classList.toggle("hidden",false); 48 | document.querySelector("#scan-output-rules").classList.toggle("hidden", false); 49 | document.querySelector("#scan-output-files").classList.toggle("hidden", false); 50 | 51 | //update navbar 52 | document.querySelector("#scan-input-nav").classList.toggle("active",false); 53 | document.querySelector("#scan-output-nav").classList.toggle("active",true); 54 | } 55 | 56 | $scope.updateIssueList = function(){ 57 | $scope.issueList = $scope.results.reduce(function(p, c) { 58 | if ((c.type == 'finding') && (typeof p !== "undefined")) { 59 | if (p.indexOf(c.rule.name) < 0) { 60 | p.push(c.rule.name); 61 | } 62 | return p; 63 | } 64 | }, []); 65 | } 66 | 67 | $scope.filterResults=function(issue) { 68 | if (!issue) { 69 | $scope.filteredResults=$scope.results; 70 | } 71 | else { 72 | if (typeof issue.name != "undefined") { 73 | $scope.filteredResults = $scope.results.filter(function(result) { 74 | return result.filename === issue.name; 75 | }); 76 | } else { 77 | $scope.filteredResults = $scope.results.filter(function(result) { 78 | return result.rule.name == issue; 79 | }); 80 | } 81 | } 82 | } 83 | 84 | $scope.navShowInput = function () { 85 | //show input tab, hide results 86 | document.querySelector("#scan-input").classList.toggle("hidden", false); 87 | document.querySelector("#scan-results").classList.toggle("hidden", true); 88 | document.querySelector("#scan-output-rules").classList.toggle("hidden", true); 89 | document.querySelector("#scan-output-files").classList.toggle("hidden", true); 90 | //make input the active nav element 91 | document.querySelector("#scan-input-nav").classList.toggle("active", true); 92 | document.querySelector("#scan-output-nav").classList.toggle("active", false); 93 | } 94 | 95 | $scope.navShowOutput = function (filterIssue) { 96 | //show input tab, hide results 97 | document.querySelector("#scan-input").classList.toggle("hidden", true); 98 | document.querySelector("#scan-results").classList.toggle("hidden", false); 99 | document.querySelector("#scan-output-rules").classList.toggle("hidden", false); 100 | document.querySelector("#scan-output-files").classList.toggle("hidden", false); 101 | //make input the active nav element 102 | document.querySelector("#scan-input-nav").classList.toggle("active", false); 103 | document.querySelector("#scan-output-nav").classList.toggle("active", true); 104 | $scope.filterResults(filterIssue); 105 | } 106 | 107 | $scope.handleFileUpload = function handleFileUpload(fileList) { 108 | function handleMaybeZip() { 109 | //packaged app case 110 | var reader = new FileReader(); 111 | $scope.inputFilename = fileList[0].name; 112 | reader.onload = function () { 113 | var magic = new DataView(this.result).getUint32(0, true /* LE */); 114 | if (magic !== 0x04034b50) { // magic marker per spec. 115 | handleList(); 116 | return; 117 | } 118 | reader.onload = function() { 119 | var zip = new JSZip(this.result); 120 | $scope.inputFiles = zip.file(/\.js$/); 121 | $scope.$apply(); 122 | }; 123 | reader.readAsArrayBuffer(fileList[0]); 124 | }; 125 | reader.readAsArrayBuffer(fileList[0].slice(0, 4)); 126 | }; 127 | 128 | function handleList() { 129 | //uploading individual js file(s) case 130 | $scope.inputFilename="Multiple files"; 131 | var jsType = /(text\/javascript|application\/javascript|application\/x-javascript)/; 132 | var zip = new JSZip(); //create a jszip to manage the files 133 | 134 | for (var i = 0; i < fileList.length; i++) { 135 | var file = fileList[i]; 136 | console.log('adding file:',file.name) 137 | if (!file.type.match(jsType)) { 138 | console.log("Ignoring non-js file:" + file.name + "(" + file.type + ")") 139 | } 140 | var reader = new FileReader(); 141 | reader.readAsText(file); 142 | 143 | reader.onload = (function (file) { 144 | var fileName = file.name; 145 | return function(e){ 146 | //add file to zip 147 | zip.file(fileName, e.target.result) 148 | $scope.inputFiles = zip.file(/.*/); //returns an array of files 149 | $scope.$apply(); 150 | 151 | }; 152 | })(file) 153 | 154 | reader.onerror = function (e) { 155 | $scope.error = "Could not read file"; 156 | $scope.$apply(); 157 | } 158 | } 159 | }; 160 | $scope.throbInput = true; 161 | $scope.$apply(); 162 | //enable fileselect div 163 | //document.querySelector("#scan-intro").classList.toggle("hidden",true); 164 | document.querySelector("#scan-files-selected").classList.toggle("hidden",false); 165 | 166 | if (fileList.length === 1) { 167 | handleMaybeZip(); 168 | } 169 | else { 170 | handleList(); 171 | } 172 | $scope.throbInput = false; 173 | $scope.$apply(); 174 | } 175 | 176 | $scope.showFile = function (index) { 177 | document.querySelector("#code-mirror-wrapper").classList.toggle("hidden",false); 178 | if($scope.inputFiles.length<1){ 179 | return; 180 | } 181 | if(!index){ 182 | index=0; 183 | } 184 | if ($scope.inputFiles.length > 0) { 185 | $scope.codeMirror.setValue($scope.inputFiles[index].asText()); 186 | } 187 | codeMirror_index = index; 188 | document.querySelector("#filename-badge").textContent = $scope.inputFiles[index].name; 189 | } 190 | 191 | $scope.showResult = function (filename,line, col) { 192 | document.querySelector("#code-mirror-wrapper").classList.toggle("hidden",false); 193 | document.querySelector("#filename-badge").textContent = filename; 194 | var file = $scope.inputFiles.find(function(f){return f.name === filename}); 195 | $scope.codeMirror.setValue(file.asText()); 196 | $scope.codeMirror.setCursor(line - 1, col || 0); 197 | $scope.codeMirror.focus(); 198 | }; 199 | 200 | $scope.saveState = function() { 201 | var includedAttributes = ['line','filename','rule', 'desc', 'name', 'rec','type']; 202 | /* A list of attributes we want include. Example: 203 | line: .. 204 | filename: .. 205 | rule: { 206 | desc: .. 207 | name: .. 208 | rec: .. 209 | type: .. 210 | } 211 | } 212 | */ 213 | var serializedResults = JSON.stringify($scope.results, includedAttributes); 214 | localforage.setItem('results', serializedResults, function() { }); 215 | 216 | var serializedErrors = JSON.stringify($scope.errors); 217 | localforage.setItem('errors', serializedErrors, function() { }); 218 | 219 | var serializedInputFiles = $scope.inputFiles.map( function(el) { return {data: el.asText(), name: el.name }; }); 220 | localforage.setItem("inputFiles", JSON.stringify(serializedInputFiles), function(r) { }); 221 | 222 | var checkboxes = []; 223 | for (var i=0; i < $scope.inputFiles.length; i++) { 224 | checkboxes.push(document.getElementById("doScan_" + i).checked); 225 | } 226 | localforage.setItem("checkboxes", JSON.stringify(checkboxes)); 227 | localforage.setItem("cm_index", JSON.stringify(codeMirror_index)); 228 | }; 229 | 230 | //TODO loadstate isn't called anymore, need to make it work with new workflow 231 | //TODO -> call loadState() around in main.js, line 36 (using the scanCtrlScope) and expose "reset" button in the UI. 232 | $scope.restoreState = function() { 233 | var apply = false; 234 | localforage.getItem('results', function (results_storage) { 235 | $scope.results = JSON.parse(results_storage); 236 | apply = true; 237 | }); 238 | localforage.getItem('errors', function (errors_storage) { 239 | if (errors_storage) { 240 | $scope.errors = JSON.parse(errors_storage); 241 | apply = true; 242 | } 243 | }); 244 | // restore files, by creating JSZip things :) 245 | localforage.getItem("inputFiles", function(inputFiles_storage) { 246 | // mimic behavior from handleFileUpload 247 | var files = JSON.parse(inputFiles_storage); 248 | var zip = new JSZip(); 249 | files.forEach(function(file) { 250 | zip.file(file.name, file.data); 251 | }); 252 | $scope.inputFiles = zip.file(/.*/); 253 | 254 | // nest checkbox import into the one for files, so we ensure the "inputFiles.length" check succeeds. 255 | localforage.getItem("checkboxes", function (checkboxes_storage) { 256 | var checkboxes = JSON.parse(checkboxes_storage); 257 | 258 | var ln=$scope.inputFiles.length 259 | for (var i=0; i < ln; i++) { 260 | document.getElementById("doScan_" + i).checked = checkboxes[i]; 261 | } 262 | }); 263 | apply = true; 264 | }); 265 | if (apply) { $scope.$apply(); } 266 | }; 267 | 268 | $scope.selectAll = function () { 269 | var element; 270 | var i = $scope.inputFiles.length-1; 271 | while (element=document.getElementById('doScan_'+i)) { 272 | element.checked = true; 273 | i--; 274 | } 275 | }; 276 | $scope.selectNone = function () { 277 | var element; 278 | var i = $scope.inputFiles.length-1; 279 | while (element=document.getElementById('doScan_'+i)) { 280 | element.checked = false; 281 | i--; 282 | } 283 | }; 284 | $scope.getSnippet = function (filename,line,numLines) { 285 | var file = $scope.inputFiles.find(function (f) { 286 | return f.name === filename 287 | }); 288 | var content=file.asText(); 289 | return content.split('\n').splice(line,line+numLines).join('\n'); 290 | }; 291 | 292 | $scope.$on('NewResults', function (event, result) { 293 | if (--pending <= 0) { $scope.throbOutput = false; } 294 | if (Object.keys(result).length === 0) { 295 | $scope.error = "Empty result set (this can also be a good thing, if you test a simple file)"; 296 | return 297 | } 298 | $scope.results=$scope.results.concat(result.findings); 299 | $scope.filteredResults=$scope.results; 300 | $scope.error = ""; 301 | $scope.updateIssueList(); 302 | /* this is likely a bug in angular or how we use it: the HTML template sometimes does not update 303 | when we change the $scope variables without it noticing. $scope.$apply() enforces this. */ 304 | $scope.$apply(); 305 | $scope.saveState(); 306 | }); 307 | 308 | $scope.$on('ScanError', function (event, exception) { 309 | if (--pending <= 0) { $scope.throbOutput = false; } 310 | $scope.errors.push(exception); 311 | $scope.updateIssueList(); 312 | $scope.$apply(); 313 | $scope.saveState(); 314 | }); 315 | 316 | }]); 317 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /common/scan.js: -------------------------------------------------------------------------------- 1 | (function (mod) { 2 | 3 | // CommonJS 4 | if (typeof exports == "object" && typeof module == "object") 5 | return mod( 6 | exports, 7 | require('../client/js/lib/walk.js') 8 | ); 9 | 10 | // AMD 11 | if (typeof define == "function" && define.amd) 12 | return define([ 13 | "exports", 14 | "../client/js/lib/walk.js" 15 | ], mod); 16 | 17 | // Plain browser env 18 | mod(this.ScanJS || (this.ScanJS = {}), this.acorn.walk); 19 | })(function (exports, walk) { 20 | "use strict"; 21 | 22 | // Default parser, override this object to change* 23 | // needs parser.parse to produce an AST 24 | // and parser.walk the walk.js lib 25 | 26 | var parser = {}; 27 | if (typeof acorn !== 'undefined' && acorn ){ 28 | parser = acorn; 29 | } 30 | 31 | 32 | var rules = {}; 33 | var results = []; 34 | 35 | var aw_found = function (rule, node) { 36 | 37 | results.push({ 38 | type: 'finding', 39 | rule: rule, 40 | filename: results.filename, 41 | line: node.loc.start.line, 42 | col: node.loc.start.col 43 | }); 44 | 45 | aw_found_callback(rule, node); 46 | }; 47 | var aw_found_callback = function () { 48 | }; 49 | 50 | var templateRules = { 51 | identifier: { 52 | nodeType: "Identifier", 53 | test: function (testNode, node) { 54 | if (node.type == "Identifier" && node.name == testNode.name) { 55 | return true; 56 | } 57 | } 58 | }, 59 | property: { 60 | nodeType: "MemberExpression", 61 | test: function (testNode, node) { 62 | // foo.bar & foo['bar'] create different AST but mean the same thing 63 | 64 | var testName = testNode.property.type == 'Identifier' ? testNode.property.name : testNode.property.value; 65 | if (node.property && (node.property.name == testName || node.property.value == testName)) { 66 | return true; 67 | } 68 | } 69 | }, 70 | object: { 71 | nodeType: "MemberExpression", 72 | test: function (testNode, node) { 73 | // foo.bar & foo['bar'] create different AST but mean the same thing 74 | if (node.object.name == testNode.object.name) { 75 | return true; 76 | } 77 | } 78 | }, 79 | objectproperty: { 80 | nodeType: "MemberExpression", 81 | test: function (testNode, node) { 82 | // foo.bar & foo['bar'] create different AST but mean the same thing 83 | var testName = testNode.property.type == 'Identifier' ? testNode.property.name : testNode.property.value; 84 | 85 | if ((node.property && (node.property.name == testName || node.property.value == testName)) && 86 | (node.object.name == testNode.object.name || // this matches the foo in foo.bar 87 | (node.object.property && node.object.property.name == testNode.object.name) )) { //this matches foo in nested objects e.g. baz.foo.bar 88 | return true; 89 | } 90 | } 91 | }, 92 | new: { 93 | nodeType: "NewExpression", 94 | test: function (testNode, node) { 95 | if (node.callee.name == testNode.callee.name) { 96 | return true; 97 | } 98 | } 99 | }, 100 | call: { 101 | nodeType: "CallExpression", 102 | test: function (testNode, node) { 103 | // matches foo() 104 | if (node.callee.type == 'Identifier' && 105 | node.callee.name == testNode.callee.name) { 106 | return true; 107 | } 108 | } 109 | }, 110 | propertycall: { 111 | nodeType: "CallExpression", 112 | test: function (testNode, node) { 113 | if (templateRules.property.test(testNode.callee, node.callee)) { 114 | return true; 115 | } 116 | } 117 | }, 118 | objectpropertycall: { 119 | nodeType: "CallExpression", 120 | test: function (testNode, node) { 121 | if (templateRules.objectproperty.test(testNode.callee, node.callee)) { 122 | return true; 123 | } 124 | } 125 | }, 126 | matchArgs: function (testNode, node) { 127 | var matching = node.arguments.length > 0; 128 | var index = 0; 129 | while (matching && index < testNode.arguments.length) { 130 | var testArg = testNode.arguments[index]; 131 | //ensure each literal argument matches 132 | if (testArg.type == "Literal") { 133 | if (typeof node.arguments[index] == 'undefined' || node.arguments[index].type != "Literal" || testArg.value != node.arguments[index].value) { 134 | matching = false; 135 | } 136 | } 137 | index++; 138 | } 139 | if (matching) { 140 | return true; 141 | } 142 | }, 143 | callargs: { 144 | nodeType: "CallExpression", 145 | test: function (testNode, node) { 146 | if (templateRules.call.test(testNode, node) && 147 | templateRules.matchArgs(testNode, node)) { 148 | return true; 149 | } 150 | } 151 | }, 152 | propertycallargs: { 153 | nodeType: "CallExpression", 154 | test: function (testNode, node) { 155 | if (templateRules.propertycall.test(testNode, node) && 156 | templateRules.matchArgs(testNode, node)) { 157 | return true; 158 | } 159 | } 160 | }, 161 | objectpropertycallargs: { 162 | nodeType: "CallExpression", 163 | test: function (testNode, node) { 164 | if (templateRules.objectpropertycall.test(testNode, node) && 165 | templateRules.matchArgs(testNode, node)) { 166 | return true; 167 | } 168 | } 169 | }, 170 | assignment: { 171 | nodeType: "AssignmentExpression", 172 | test: function (testNode, node) { 173 | if (templateRules.identifier.test(testNode.left, node.left)) { 174 | //support $_unsafe for RHS of assignment 175 | var unsafe = true; 176 | if (testNode.right.type == "Identifier" && testNode.right.name == "$_unsafe") { 177 | unsafe = templateRules.$_contains(node.right, "Identifier") 178 | } 179 | return unsafe; 180 | } 181 | } 182 | }, 183 | propertyassignment: { 184 | nodeType: "AssignmentExpression", 185 | test: function (testNode, node) { 186 | //support $_unsafe for RHS of assignment 187 | var unsafe = true; 188 | if (testNode.right.type == "Identifier" && testNode.right.name == "$_unsafe") { 189 | unsafe = templateRules.$_contains(node.right, "Identifier") 190 | } 191 | 192 | if (templateRules.property.test(testNode.left, node.left) && unsafe) { 193 | return true; 194 | } 195 | } 196 | }, 197 | objectpropertyassignment: { 198 | nodeType: "AssignmentExpression", 199 | test: function (testNode, node) { 200 | 201 | //support $_unsafe for RHS of assignment 202 | var unsafe = true; 203 | if (testNode.right.type == "Identifier" && testNode.right.name == "$_unsafe") { 204 | unsafe = templateRules.$_contains(node.right, "Identifier") 205 | } 206 | 207 | if (templateRules.objectproperty.test(testNode.left, node.left) && unsafe) { 208 | return true; 209 | } 210 | } 211 | }, 212 | $_contains: function (node, typestring) { 213 | var foundnode = walk.findNodeAt(node, null, null, typestring); 214 | return typeof foundnode != 'undefined' 215 | }, 216 | ifstatement: { 217 | nodeType: "IfStatement", 218 | test: function (testNode, node) { 219 | if (testNode.test.type == "CallExpression" && testNode.test.callee.name == "$_contains") { 220 | if (testNode.test.arguments[0].type == "Literal") { 221 | if (templateRules.$_contains(node.test, testNode.test.arguments[0].value)) { 222 | return true; 223 | } 224 | } 225 | } 226 | } 227 | } 228 | }; 229 | 230 | function aw_loadRulesFile(rulesFile, callback) { 231 | 232 | var request = new XMLHttpRequest(); 233 | 234 | request.open('GET', rulesFile, false); 235 | 236 | request.onload = function () { 237 | if (request.status >= 200 && request.status < 400) { 238 | rulesData = JSON.parse(request.responseText); 239 | aw_loadRules(rulesData); 240 | if (typeof callback == 'function') 241 | callback(rules); 242 | } else { 243 | console.log('Error loading ' + rules) 244 | } 245 | }; 246 | 247 | request.onerror = function () { 248 | console.log('Connection error while loading ' + rulesFile) 249 | }; 250 | request.send(); 251 | } 252 | 253 | 254 | function aw_parseRule(rule) { 255 | try { 256 | var program = parser.parse(rule.source); 257 | //each rule must contain exactly one javascript statement 258 | if (program.body.length != 1) { 259 | throw ('Rule ' + rule.name + 'contains too many statements, skipping: ' + rule.source); 260 | 261 | } 262 | rule.statement = program.body[0] 263 | } catch (e) { 264 | throw('Can\'t parse rule:' + rule.name ); 265 | } 266 | 267 | if (rule.statement.type == "IfStatement") { 268 | return 'ifstatement'; 269 | } 270 | 271 | //identifier 272 | if (rule.statement.expression.type == "Identifier") { 273 | return 'identifier'; 274 | } 275 | 276 | if (rule.statement.expression.type == "NewExpression") { 277 | return 'new'; 278 | } 279 | 280 | //property, objectproperty 281 | if (rule.statement.expression.type == "MemberExpression") { 282 | if (rule.statement.expression.object.name == "$_any") { 283 | //rule is $_any.foo, this is a property rule 284 | return 'property'; 285 | } else if (rule.statement.expression.property.name == "$_any") { 286 | return 'object'; 287 | } 288 | else { 289 | return 'objectproperty'; 290 | } 291 | } 292 | //call, propertycall,objectpropertycall ( + args variants) 293 | if (rule.statement.expression.type == "CallExpression") { 294 | var args = ''; 295 | if (rule.statement.expression.arguments.length > 0) { 296 | args = 'args'; 297 | } 298 | 299 | if (rule.statement.expression.callee.type == "Identifier") { 300 | return 'call' + args; 301 | } else if (rule.statement.expression.callee.type == "MemberExpression") { 302 | if (rule.statement.expression.callee.object.name == "$_any") { 303 | return 'propertycall' + args; 304 | } else { 305 | return 'objectpropertycall' + args; 306 | } 307 | } 308 | } 309 | 310 | //assignment, propertyassignment, objectpropertyassignment 311 | if (rule.statement.expression.type == "AssignmentExpression") { 312 | if (rule.statement.expression.left.type == "MemberExpression") { 313 | if (rule.statement.expression.left.object.name == "$_any") { 314 | return 'propertyassignment'; 315 | } else { 316 | return 'objectpropertyassignment'; 317 | } 318 | } else { 319 | return 'assignment'; 320 | } 321 | } 322 | 323 | 324 | //if we get to here we couldn't find a matching template for the rule.source 325 | throw ("No matching template") 326 | } 327 | 328 | function aw_loadRules(rulesData) { 329 | 330 | var nodeTests = {}; 331 | //each node type may have multiple tests, so first create arrays of test funcs 332 | for (var i in rulesData) { 333 | var rule = rulesData[i]; 334 | //parse rule source 335 | var template; 336 | try { 337 | template = templateRules[aw_parseRule(rule)]; 338 | } catch (e) { 339 | console.log("Error parsing rule " + rule.name, rule.source) 340 | break; 341 | } 342 | 343 | if (typeof template == 'undefined') { 344 | console.log("Should never get here.") 345 | break; 346 | } 347 | 348 | if (!nodeTests[template.nodeType]) { 349 | nodeTests[template.nodeType] = []; 350 | } 351 | nodeTests[template.nodeType].push(function (template, rule, node) { 352 | if (rule.statement.expression) { 353 | if (template.test(rule.statement.expression, node)) { 354 | aw_found(rule, node); 355 | } 356 | } 357 | else { 358 | if (template.test(rule.statement, node)) { 359 | aw_found(rule, node); 360 | } 361 | } 362 | }.bind(undefined, template, rule)); 363 | } 364 | 365 | 366 | rules = {}; 367 | //create a single function for each nodeType, which calls all the test functions 368 | for (var nodeType in nodeTests) { 369 | rules[nodeType] = function (tests, node) { 370 | tests.forEach(function (test) { 371 | test.call(this, node); 372 | }); 373 | }.bind(undefined, nodeTests[nodeType]); 374 | } 375 | } 376 | 377 | function aw_scan(ast, filename) { 378 | results = []; 379 | results.filename = "Manual input" 380 | 381 | if (typeof filename != 'undefined') { 382 | results.filename = filename; 383 | } 384 | if (!rules) { 385 | return [ 386 | { 387 | type: 'error', 388 | name: 'RulesError', 389 | pos: 0, 390 | loc: { column: 0, line: 0 }, 391 | message: "Could not scan with no rules loaded.", 392 | filename: filename 393 | } 394 | ]; 395 | } 396 | walk.simple(ast, rules); 397 | 398 | return results; 399 | } 400 | 401 | function aw_setCallback(found_callback) { 402 | aw_found_callback = found_callback; 403 | } 404 | 405 | function aw_setParser(newParser){ 406 | parser = newParser; 407 | } 408 | 409 | exports.rules = rules; 410 | exports.scan = aw_scan; 411 | exports.loadRulesFile = aw_loadRulesFile; 412 | exports.loadRules = aw_loadRules; 413 | exports.parseRule = aw_parseRule; 414 | exports.setResultCallback = aw_setCallback; 415 | exports.parser = aw_setParser; 416 | 417 | }); 418 | -------------------------------------------------------------------------------- /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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).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(acorn.parse(bad, {locations : true}), "/tests/")).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(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 190 | }); 191 | }); 192 | }); 193 | context('dangerous', function () { 194 | /* deactivated, failing test. 195 | context(null, function () { 196 | var bad = 'mozKeyboard.onfocuschange = alert(0);'; 197 | it(bad, function () { 198 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 199 | }); 200 | }); 201 | */ 202 | }); 203 | }); 204 | 205 | context('mozMobileConnection', function () { 206 | context('safe', function () { 207 | context(null, function () { 208 | var good = 'var a = "mozMobileConnection";'; 209 | it(good, function () { 210 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 211 | }); 212 | }); 213 | }); 214 | context('dangerous', function () { 215 | /* deactivated, failing test. 216 | context(null, function () { 217 | var bad = 'MozMobileConnection.sendMMI()'; 218 | it(bad, function () { 219 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 220 | }); 221 | }); 222 | */ 223 | }); 224 | }); 225 | 226 | context('.mozNotification', function () { 227 | context('safe', function () { 228 | context(null, function () { 229 | var good = 'var mozNotification = ".mozNotification, this is just a string";'; 230 | it(good, function () { 231 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 232 | }); 233 | }); 234 | }); 235 | context('dangerous', function () { 236 | context(null, function () { 237 | var bad = "if (window.webkitNotifications) { _popup = window; } else if (navigator.mozNotification) { console.log (1); }"; 238 | it(bad, function () { 239 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 240 | }); 241 | }); 242 | }); 243 | }); 244 | 245 | context('mozPermissionSettings', function () { 246 | context('safe', function () { 247 | context(null, function () { 248 | var good = 'var a = "just a string, not mozPermissionSettings";'; 249 | it(good, function () { 250 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 251 | }); 252 | }); 253 | context(null, function () { 254 | var good = 'var mozPermissionSettings = "just a string, not mozPermissionSettings";'; 255 | it(good, function () { 256 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 257 | }); 258 | }); 259 | }); 260 | context('dangerous', function () { 261 | context(null, function () { 262 | var bad = 'var permissions = window.navigator.mozPermissionSettings;'; 263 | it(bad, function () { 264 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 265 | }); 266 | }); 267 | }); 268 | }); 269 | 270 | context('mozPower', function () { 271 | context('safe', function () { 272 | context(null, function () { 273 | var good = 'var a = "just a string, window.navigator.mozPower;";'; 274 | it(good, function () { 275 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 276 | }); 277 | }); 278 | context(null, function () { 279 | var good = 'var mozPower = "just a string, window.navigator.mozPower;";'; 280 | it(good, function () { 281 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 282 | }); 283 | }); 284 | }); 285 | context('dangerous', function () { 286 | context(null, function () { 287 | var bad = 'var power = window.navigator.mozPower;'; 288 | it(bad, function () { 289 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 290 | }); 291 | }); 292 | }); 293 | }); 294 | 295 | context('mozSetMessageHandler', function () { 296 | context('safe', function () { 297 | context(null, function () { 298 | var good = 'var a = "static mozSetMessageHandler";'; 299 | it(good, function () { 300 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 301 | }); 302 | }); 303 | }); 304 | context('dangerous', function () { 305 | context(null, function () { 306 | var bad = 'navigator.mozSetMessageHandler(type, handler);'; 307 | it(bad, function () { 308 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 309 | }); 310 | }); 311 | }); 312 | }); 313 | 314 | context('mozSettings', function () { 315 | context('safe', function () { 316 | context(null, function () { 317 | var good = 'var a = "window.navigator.mozSettings;";'; 318 | it(good, function () { 319 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 320 | }); 321 | }); 322 | context(null, function () { 323 | var good = 'var mozSettings = "window.navigator.mozSettings;";'; 324 | it(good, function () { 325 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 326 | }); 327 | }); 328 | }); 329 | context('dangerous', function () { 330 | context(null, function () { 331 | var bad = 'var settings = window.navigator.mozSettings;'; 332 | it(bad, function () { 333 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 334 | }); 335 | }); 336 | }); 337 | }); 338 | 339 | context('mozSms', function () { 340 | context('safe', function () { 341 | context(null, function () { 342 | var good = 'var a = "window.navigator.mozSms;";'; 343 | it(good, function () { 344 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 345 | }); 346 | }); 347 | context(null, function () { 348 | var good = 'var mozSms = "window.navigator.mozSms;";'; 349 | it(good, function () { 350 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 351 | }); 352 | }); 353 | }); 354 | context('dangerous', function () { 355 | /* deactivated, failing test. 356 | context(null, function () { 357 | var bad = 'var sms = window.navigator.mozSms;'; 358 | it(bad, function () { 359 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 360 | }); 361 | }); 362 | */ 363 | /* deactivated, failing test. 364 | context(null, function () { 365 | var bad = 'var msg = window.navigator.mozSms.getMessage(1);'; 366 | it(bad, function () { 367 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 368 | }); 369 | }); 370 | */ 371 | }); 372 | }); 373 | 374 | context('mozSystem', function () { 375 | context('safe', function () { 376 | context(null, function () { 377 | var good = 'var a = "mozSystem: true";'; 378 | it(good, function () { 379 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 380 | }); 381 | }); 382 | }); 383 | context('dangerous', function () { 384 | context(null, function () { 385 | var bad = 'var xhr = new XMLHttpRequest({ mozSystem: true});'; 386 | it(bad, function () { 387 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 388 | }); 389 | }); 390 | }); 391 | }); 392 | 393 | context('.mozTCPSocket', function () { 394 | context('safe', function () { 395 | context(null, function () { 396 | var good = 'var a = "navigator.mozTCPSocket;"'; 397 | it(good, function () { 398 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 399 | }); 400 | }); 401 | }); 402 | context('dangerous', function () { 403 | context(null, function () { 404 | var bad = 'var socket = navigator.mozTCPSocket;'; 405 | it(bad, function () { 406 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 407 | }); 408 | }); 409 | }); 410 | }); 411 | 412 | context('mozTelephony', function () { 413 | context('safe', function () { 414 | context(null, function () { 415 | var good = 'var a = "window.navigator.mozTelephony;";'; 416 | it(good, function () { 417 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 418 | }); 419 | }); 420 | }); 421 | context('dangerous', function () { 422 | context(null, function () { 423 | var bad = 'var phone = window.navigator.mozTelephony;'; 424 | it(bad, function () { 425 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 426 | }); 427 | }); 428 | }); 429 | }); 430 | 431 | context('mozTime', function () { 432 | context('safe', function () { 433 | context(null, function () { 434 | var good = 'var a = "mozTime;";'; 435 | it(good, function () { 436 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 437 | }); 438 | }); 439 | }); 440 | context('dangerous', function () { 441 | context(null, function () { 442 | var bad = 'var time = window.navigator.mozTime;'; 443 | it(bad, function () { 444 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 445 | }); 446 | }); 447 | }); 448 | }); 449 | 450 | context('mozVoicemail', function () { 451 | context('safe', function () { 452 | context(null, function () { 453 | var good = 'var a = "mozVoicemail";'; 454 | it(good, function () { 455 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 456 | }); 457 | }); 458 | }); 459 | context('dangerous', function () { 460 | context(null, function () { 461 | var bad = 'var voicemail = window.navigator.mozVoicemail;'; 462 | it(bad, function () { 463 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 464 | }); 465 | }); 466 | context(null, function () { 467 | var bad = 'var status = window.navigator.mozVoicemail.getStatus();'; 468 | it(bad, function () { 469 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 470 | }); 471 | }); 472 | }); 473 | }); 474 | 475 | context('mozapp', function () { 476 | context('safe', function () { 477 | context(null, function () { 478 | var good = 'var a = "mozapp";'; 479 | it(good, function () { 480 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 481 | }); 482 | }); 483 | }); 484 | context('dangerous', function () { 485 | /* deactivated, failing test. 486 | context(null, function () { 487 | var bad = 'var a = document.createElement("iframe"); a.mozapp = data.app; document.appendChild(a);'; 488 | it(bad, function () { 489 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 490 | }); 491 | }); 492 | */ 493 | context(null, function () { 494 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 495 | var bad = 'iframe.setAttribute("mozapp", data.app);'; 496 | it.skip(bad, function () { 497 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 498 | }); 499 | }); 500 | }); 501 | }); 502 | 503 | context('mozaudiochannel', function () { 504 | context('safe', function () { 505 | context(null, function () { 506 | var good = 'var a = "mozaudiochannel=content.toString()";'; 507 | it(good, function () { 508 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 509 | }); 510 | }); 511 | }); 512 | context('dangerous', function () { 513 | /* deactivated, failing test. 514 | context(null, function () { 515 | var bad = 'var a = document.createElement("audio"); a.mozaudiochannel = "content"; document.appendChild(a);'; 516 | it(bad, function () { 517 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 518 | }); 519 | }); 520 | */ 521 | context(null, function () { 522 | // issue 73 - https://github.com/mozilla/scanjs/issues/73 523 | var bad = 'var a = document.createElement("audio"); a.setAttribute("mozaudiochannel", data.app);'; 524 | it.skip(bad, function () { 525 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 526 | }); 527 | }); 528 | }); 529 | }); 530 | 531 | context('moznetworkdownload', function () { 532 | context('safe', function () { 533 | context(null, function () { 534 | var good = 'var a = "moznetworkdownload";'; 535 | it(good, function () { 536 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 537 | }); 538 | }); 539 | }); 540 | context('dangerous', function () { 541 | /* deactivated, failing test. 542 | context(null, function () { 543 | var bad = 'addEventListener("moznetworkdownload", downloadHandler);'; 544 | it(bad, function () { 545 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 546 | }); 547 | }); 548 | */ 549 | }); 550 | }); 551 | 552 | context('moznetworkupload', function () { 553 | context('safe', function () { 554 | context(null, function () { 555 | var good = 'var a = "moznetworkupload";'; 556 | it(good, function () { 557 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 558 | }); 559 | }); 560 | }); 561 | context('dangerous', function () { 562 | /* deactivated, failing test. 563 | context(null, function () { 564 | var bad = 'addEventListener("moznetworkupload", downloadHandler);'; 565 | it(bad, function () { 566 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 567 | }); 568 | }); 569 | */ 570 | }); 571 | }); 572 | 573 | context('mozWifiManager', function () { 574 | context('safe', function () { 575 | context(null, function () { 576 | var good = 'var a = "mozWifiManager";'; 577 | it(good, function () { 578 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 579 | }); 580 | }); 581 | context(null, function () { 582 | var good = 'somethingNotNavigator.mozWifiManager;'; 583 | it(good, function () { 584 | chai.expect(ScanJS.scan(acorn.parse(good, {locations : true}), "/tests/")).to.be.empty; 585 | }); 586 | }); 587 | }); 588 | context('dangerous', function () { 589 | context(null, function () { 590 | var bad = 'var wifi = navigator.mozWifiManager;'; 591 | it(bad, function () { 592 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 593 | }); 594 | }); 595 | context(null, function () { 596 | var bad = 'var networks = navigator.mozWifiManager.getNetworks();'; 597 | it(bad, function () { 598 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 599 | }); 600 | }); 601 | context(null, function () { 602 | var bad = 'var wifi = navigator.mozWifiManager;'; 603 | it(bad, function () { 604 | chai.expect(ScanJS.scan(acorn.parse(bad, {locations : true}), "/tests/")).not.to.be.empty; 605 | }); 606 | }); 607 | }); 608 | }); 609 | 610 | })(); //describe('Mozilla specific tests'... 611 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------