├── .npmignore ├── dist ├── js │ ├── libraries │ │ ├── wappalyzer.js │ │ ├── VulnerableApplicationLibrary.js │ │ ├── Library.js │ │ ├── ApplicationLibrary.js │ │ └── showLibraries.js │ ├── models │ │ ├── contrastCredentials.js │ │ ├── DomainStorage.js │ │ ├── VulnerableTab.js │ │ ├── Application.js │ │ ├── ConnectedDomain.js │ │ ├── Vulnerability.js │ │ ├── ApplicationTable.js │ │ └── PopupTableRow.js │ ├── popup.js │ ├── index.js │ ├── queue.js │ ├── content-scripts │ │ ├── ContrastForms.js │ │ └── content-script.js │ ├── popupMethods.js │ └── background.js ├── img │ ├── icon.png │ ├── ring-alt.gif │ ├── contrast-on.png │ ├── contrast128.png │ ├── contrast16.png │ ├── contrast48.png │ ├── getStarted.png │ ├── contrast-off.png │ ├── 403Unauthorized.jpg │ ├── 404PageNotFound.jpg │ ├── glyphicons-cog.png │ ├── loginBackground.png │ ├── contrastLogoLarge.png │ ├── contrast-not-configured.png │ └── trash.svg ├── html │ └── background.html ├── manifest.json └── scripts │ ├── md5.min.js │ └── murmurHash3.min.js ├── test ├── spec │ ├── content-script-spec.js │ ├── models │ │ ├── ApplicationTable-spec.js │ │ ├── ContrastCredentials-spec.js │ │ ├── Application-spec.js │ │ └── Config-spec.js │ └── settings-spec.js ├── __setups__ │ └── chrome.js ├── mocks │ └── storageMock.js └── snapshots │ └── index.test.js ├── img ├── icon.png ├── ring-alt.gif ├── contrast-on.png ├── contrast128.png ├── contrast16.png ├── contrast48.png ├── getStarted.png ├── contrast-off.png ├── 403Unauthorized.jpg ├── 404PageNotFound.jpg ├── glyphicons-cog.png ├── loginBackground.png ├── contrastLogoLarge.png ├── contrast-not-configured.png └── trash.svg ├── .babelrc ├── .gitignore ├── .travis.yml ├── .prettierrc.json ├── html └── background.html ├── wapp-service ├── package.json └── index.js ├── setup.sh ├── scripts ├── deploy.sh ├── test.sh ├── pre_push.sh ├── bab.sh ├── md5.min.js └── murmurHash3.min.js ├── gulpfile.js ├── js ├── libraries │ ├── wappalyzer.js │ ├── VulnerableApplicationLibrary.js │ ├── Library.js │ ├── ApplicationLibrary.js │ └── showLibraries.js ├── models │ ├── contrastCredentials.js │ ├── DomainStorage.js │ ├── VulnerableTab.js │ ├── ConnectedDomain.js │ ├── Application.js │ ├── Vulnerability.js │ └── PopupTableRow.js ├── popup.js ├── queue.js ├── index.js ├── popupMethods.js └── content-scripts │ └── ContrastForms.js ├── lib ├── libraries │ ├── wappalyzer.js │ ├── VulnerableApplicationLibrary.js │ └── Library.js ├── models │ ├── contrastCredentials.js │ ├── DomainStorage.js │ ├── VulnerableTab.js │ ├── ConnectedDomain.js │ └── Application.js ├── popup.js ├── popupMethods.js ├── content-scripts │ └── ContrastForms.js └── index.js ├── package.json ├── manifest.json ├── README.md └── requirements.md /.npmignore: -------------------------------------------------------------------------------- 1 | wapp-service/ 2 | -------------------------------------------------------------------------------- /dist/js/libraries/wappalyzer.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/spec/content-script-spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__setups__/chrome.js: -------------------------------------------------------------------------------- 1 | require('jest-webextension-mock'); 2 | -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/icon.png -------------------------------------------------------------------------------- /img/ring-alt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/ring-alt.gif -------------------------------------------------------------------------------- /dist/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/icon.png -------------------------------------------------------------------------------- /img/contrast-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast-on.png -------------------------------------------------------------------------------- /img/contrast128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast128.png -------------------------------------------------------------------------------- /img/contrast16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast16.png -------------------------------------------------------------------------------- /img/contrast48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast48.png -------------------------------------------------------------------------------- /img/getStarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/getStarted.png -------------------------------------------------------------------------------- /dist/img/ring-alt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/ring-alt.gif -------------------------------------------------------------------------------- /img/contrast-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast-off.png -------------------------------------------------------------------------------- /dist/img/contrast-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast-on.png -------------------------------------------------------------------------------- /dist/img/contrast128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast128.png -------------------------------------------------------------------------------- /dist/img/contrast16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast16.png -------------------------------------------------------------------------------- /dist/img/contrast48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast48.png -------------------------------------------------------------------------------- /dist/img/getStarted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/getStarted.png -------------------------------------------------------------------------------- /img/403Unauthorized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/403Unauthorized.jpg -------------------------------------------------------------------------------- /img/404PageNotFound.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/404PageNotFound.jpg -------------------------------------------------------------------------------- /img/glyphicons-cog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/glyphicons-cog.png -------------------------------------------------------------------------------- /img/loginBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/loginBackground.png -------------------------------------------------------------------------------- /dist/img/contrast-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast-off.png -------------------------------------------------------------------------------- /img/contrastLogoLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrastLogoLarge.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | "transform-runtime", 5 | "transform-async-to-generator" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /dist/img/403Unauthorized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/403Unauthorized.jpg -------------------------------------------------------------------------------- /dist/img/404PageNotFound.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/404PageNotFound.jpg -------------------------------------------------------------------------------- /dist/img/glyphicons-cog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/glyphicons-cog.png -------------------------------------------------------------------------------- /dist/img/loginBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/loginBackground.png -------------------------------------------------------------------------------- /dist/img/contrastLogoLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrastLogoLarge.png -------------------------------------------------------------------------------- /img/contrast-not-configured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/img/contrast-not-configured.png -------------------------------------------------------------------------------- /dist/img/contrast-not-configured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/contrast-chrome-extension/master/dist/img/contrast-not-configured.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | tests_output 5 | 6 | dist/* 7 | dist.zip 8 | !dist/.keep 9 | 10 | builds/* 11 | !builds/.keep 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - '8' 6 | before_install: 7 | - npm install -g mocha eslint 8 | - npm install 9 | script: 10 | - npm run bab && eslint js/ 11 | -------------------------------------------------------------------------------- /img/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dist/img/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": true, 8 | "arrowParens": "always", 9 | "insertPragma": true, 10 | "printWidth": 80, 11 | "proseWrap": "always" 12 | } 13 | -------------------------------------------------------------------------------- /html/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /dist/html/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /wapp-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wapp-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.3", 14 | "wappalyzer": "^5.5.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dist/js/models/contrastCredentials.js: -------------------------------------------------------------------------------- 1 | import{CONTRAST_USERNAME,CONTRAST_SERVICE_KEY,CONTRAST_API_KEY,CONTRAST_ORG_UUID,TEAMSERVER_URL}from"../util.js";class ContrastCredentials{constructor(R){const{apiKey:t,orgUuid:E,teamServerUrl:T,serviceKey:_,profileEmail:r}=R;this[CONTRAST_API_KEY]=t,this[CONTRAST_ORG_UUID]=E,this[TEAMSERVER_URL]=T,this[CONTRAST_SERVICE_KEY]=_,this[CONTRAST_USERNAME]=r}}export default ContrastCredentials; -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | GIT_DIR=$(git rev-parse --git-dir) 2 | echo "Installing Git hooks for development..." 3 | # this command creates symlink to our pre-commit script 4 | if [ -L $GIT_DIR/hooks/pre-push ]; then 5 | echo "Git hooks found, skipping" 6 | else 7 | ln -s ../../scripts/pre_push.sh $GIT_DIR/hooks/pre-push 8 | chmod +x scripts/pre_push.sh 9 | fi 10 | 11 | # echo "Installing Wappalyzer service..." 12 | # cd wapp-service 13 | # npm install 14 | # node index.js & 15 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | PROJECT_ROOT=$(pwd) 4 | DIST_DIR="$PROJECT_ROOT/dist" 5 | 6 | rm -rf $DIST_DIR 7 | mkdir -p $DIST_DIR 8 | mkdir -p $DIST_DIR/js 9 | 10 | # cp -r js $DIST_DIR 11 | npm run gulp 12 | cp -r img $DIST_DIR 13 | cp -r html $DIST_DIR 14 | cp -r scripts $DIST_DIR 15 | cp style.css $DIST_DIR 16 | cp manifest.json $DIST_DIR 17 | 18 | rm -rfv $DIST_DIR/scripts/*.sh 19 | 20 | timestamp=$(date +"%Y-%m-%d_%H-%M-%S") 21 | 22 | mkdir -p $PROJECT_ROOT/builds 23 | zip -r $PROJECT_ROOT/builds/contrast-${timestamp}.zip $DIST_DIR 24 | -------------------------------------------------------------------------------- /test/mocks/storageMock.js: -------------------------------------------------------------------------------- 1 | const storage = {}; 2 | const storageMock = { 3 | setItem: function(key, value) { 4 | storage[key] = value || ''; 5 | }, 6 | getItem: function(key) { 7 | return key in storage ? storage[key] : null; 8 | }, 9 | removeItem: function(key) { 10 | delete storage[key]; 11 | }, 12 | get length() { 13 | return Object.keys(storage).length; 14 | }, 15 | key: function(i) { 16 | var keys = Object.keys(storage); 17 | return keys[i] || null; 18 | } 19 | } 20 | 21 | module.exports = storageMock; 22 | -------------------------------------------------------------------------------- /dist/js/models/DomainStorage.js: -------------------------------------------------------------------------------- 1 | import{CONNECTED_APP_DOMAINS}from"../util.js";class DomainStorage{constructor(){const t=this._getDomainsFromStorage();this.domains=["http://localhost:*/*"].concat(t)}_getDomainsFromStorage(){const t=window.localStorage.getItem(CONNECTED_APP_DOMAINS);return t?"string"==typeof t?JSON.parse(t):t:[]}addDomainsToStorage(t){let o=this._getDomainsFromStorage();o=o.concat(t),this._setNewDomains(o)}removeDomainsFromStorage(t){let o=this._getDomainsFromStorage();o=o.filter(o=>!t.includes(o)),this._setNewDomains(o)}_setNewDomains(t){this.domains=t,window.localStorage.setItem(CONNECTED_APP_DOMAINS,JSON.stringify(t))}}export default DomainStorage; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | // const uglify = require('gulp-uglify'); 3 | const uglify = require('gulp-uglify-es').default; 4 | const pump = require('pump'); 5 | 6 | console.log("Running Gulp"); 7 | 8 | gulp.task('default', ['compress']) 9 | 10 | gulp.task('compress', function (cb) { 11 | console.log("Gulp Compressing"); 12 | pump([ 13 | gulp.src('js/**/*.js'), 14 | uglify({ 15 | warnings: "verbose", 16 | }), 17 | gulp.dest('dist/js') 18 | ], 19 | cb 20 | ); 21 | }); 22 | 23 | // function callback() { 24 | // console.log("Gulp Callback"); 25 | // } 26 | // gulp.tasks.compress.fn(callback); 27 | -------------------------------------------------------------------------------- /js/libraries/wappalyzer.js: -------------------------------------------------------------------------------- 1 | // /*global 2 | // chrome, 3 | // */ 4 | // 5 | // import { WAPPALYZER_SERVICE } from "../util.js"; 6 | // 7 | // const wappalzye = async tab => { 8 | // const tabURL = new URL(tab.url); 9 | // const response = await fetch(WAPPALYZER_SERVICE + "?site=" + tabURL.href); 10 | // if (response.ok && response.status === 200) { 11 | // const json = await response.json(); 12 | // if (json.success) { 13 | // return json.libraries.applications; 14 | // } 15 | // return null; 16 | // } 17 | // // console.log("ERROR IN WAPPALYZE RESPONSE", response); 18 | // return null; 19 | // }; 20 | // 21 | // export { wappalzye }; 22 | -------------------------------------------------------------------------------- /lib/libraries/wappalyzer.js: -------------------------------------------------------------------------------- 1 | // /*global 2 | // chrome, 3 | // */ 4 | // 5 | // import { WAPPALYZER_SERVICE } from "../util.js"; 6 | // 7 | // const wappalzye = async tab => { 8 | // const tabURL = new URL(tab.url); 9 | // const response = await fetch(WAPPALYZER_SERVICE + "?site=" + tabURL.href); 10 | // if (response.ok && response.status === 200) { 11 | // const json = await response.json(); 12 | // if (json.success) { 13 | // return json.libraries.applications; 14 | // } 15 | // return null; 16 | // } 17 | // // console.log("ERROR IN WAPPALYZE RESPONSE", response); 18 | // return null; 19 | // }; 20 | // 21 | // export { wappalzye }; 22 | "use strict"; -------------------------------------------------------------------------------- /js/models/contrastCredentials.js: -------------------------------------------------------------------------------- 1 | import { 2 | CONTRAST_USERNAME, 3 | CONTRAST_SERVICE_KEY, 4 | CONTRAST_API_KEY, 5 | CONTRAST_ORG_UUID, 6 | TEAMSERVER_URL, 7 | } from '../util.js'; 8 | 9 | class ContrastCredentials { 10 | constructor(options) { 11 | const { 12 | apiKey, 13 | orgUuid, 14 | teamServerUrl, 15 | serviceKey, 16 | profileEmail, 17 | } = options; 18 | this[CONTRAST_API_KEY] = apiKey; 19 | this[CONTRAST_ORG_UUID] = orgUuid; 20 | this[TEAMSERVER_URL] = teamServerUrl; 21 | this[CONTRAST_SERVICE_KEY] = serviceKey; 22 | this[CONTRAST_USERNAME] = profileEmail; 23 | } 24 | } 25 | 26 | export default ContrastCredentials; 27 | -------------------------------------------------------------------------------- /test/spec/models/ApplicationTable-spec.js: -------------------------------------------------------------------------------- 1 | const chrome = require("sinon-chrome/extensions"); 2 | global.chrome = chrome; 3 | 4 | const url = require("url"); 5 | global.URL = url.URL; 6 | 7 | const jsdom = require("jsdom"); 8 | const { JSDOM } = jsdom; 9 | global.window = new JSDOM("", { url: "http://localhost" }).window; 10 | global.document = global.window.document; 11 | 12 | const sinon = require("sinon"); 13 | const chai = require("chai"); 14 | const { expect } = chai; 15 | const util = require('../../../lib/util.js'); 16 | const testData = require('../../testData.js'); 17 | const ApplicationTable = require('../../../lib/models/ApplicationTable.js').default; 18 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$(pwd) 4 | SNAP_DIR="__snapshots__" 5 | 6 | echo "$(tput setaf 2) Running Mocha tests...$(tput sgr0)" 7 | node ./node_modules/nyc/bin/nyc.js ./node_modules/mocha/bin/mocha ${DIR}/test/spec/* 8 | echo "" 9 | 10 | echo "$(tput setaf 2) Creating a new __snapshots__ folder in project root.$(tput sgr0)" 11 | mkdir -p ${DIR}/${SNAP_DIR} 12 | echo "" 13 | 14 | echo "$(tput setaf 2) Copying snapshot files from test dir to snapshot dir.$(tput sgr0)" 15 | cp -r ${DIR}/test/${SNAP_DIR}/ ${DIR}/${SNAP_DIR} 16 | echo "" 17 | 18 | echo "$(tput setaf 2) Running Snapshot tests...$(tput sgr0)" 19 | ./node_modules/mocha/bin/mocha test/snapshots/* 20 | echo "" 21 | 22 | echo "$(tput setaf 2) Cleaning up snapshot directory in project root. $(tput sgr0)" 23 | rm -rf ${DIR}/${SNAP_DIR} 24 | echo "" 25 | -------------------------------------------------------------------------------- /dist/js/popup.js: -------------------------------------------------------------------------------- 1 | import{getStorageVulnsAndRender,hideLoadingIcon}from"./popupMethods.js";import{STORED_APPS_KEY,getStoredCredentials,isCredentialed,getHostFromUrl,isEmptyObject,setElementDisplay}from"./util.js";function renderLoadingIcon(){document.getElementById("vulns-loading").style.display="block"}document.addEventListener("DOMContentLoaded",()=>{chrome.storage.local.get(STORED_APPS_KEY,e=>{chrome.tabs.query({active:!0,currentWindow:!0},t=>{if(!t[0])return;const n=t[0],o=new URL(n.url),r=getHostFromUrl(o),d=e[STORED_APPS_KEY],i=d?d.filter(e=>e[r])[0]:d;if(i&&!isEmptyObject(i))getStoredCredentials().then(e=>{if(!isCredentialed(e))throw new Error("Not Credentialed");renderLoadingIcon(),getStorageVulnsAndRender(e,i,n)}).catch(e=>new Error(e));else{const e=document.getElementById("vulnerabilities-found-on-page");setElementDisplay(e,"none"),hideLoadingIcon()}})})},!1); -------------------------------------------------------------------------------- /dist/js/index.js: -------------------------------------------------------------------------------- 1 | import{getStoredCredentials,isCredentialed}from"./util.js";import Application from"./models/Application.js";import ApplicationTable from"./models/ApplicationTable.js";import Config from"./models/Config.js";export function indexFunction(){chrome.tabs.query({active:!0,currentWindow:!0},e=>{const n=e[0],t=new URL(n.url);getStoredCredentials().then(async e=>{const o=isCredentialed(e),i=await Application.retrieveApplicationFromStorage(n),r=new Config(n,t,o,e,!!i);if(r.addListenerToConfigButton(),r.popupScreen(),o){if(o&&r._isContrastPage()){new ApplicationTable(t).renderApplicationsMenu(),r.setGearIcon(),r.renderContrastUsername()}else if(r.setGearIcon(),r.renderContrastUsername(),!r._isContrastPage()){const e=new ApplicationTable(t);e.renderActivityFeed(),r.hasApp&&e.renderApplicationsMenu()}}else console.log("Please Configure the Extension")}).catch(e=>new Error(e))})}document.addEventListener("DOMContentLoaded",indexFunction,!1); -------------------------------------------------------------------------------- /scripts/pre_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # NOTE: YOU MUST INSTALL GNU SED FIRST 4 | # With Homebrew: brew install gnu-sed 5 | 6 | # get files that have console.log 7 | # console_log_files=$(grep -lr 'console.log' js/*.js) 8 | 9 | # search lines with console.log in console_log_files and remove the line 10 | # for file in $console_log_files; do 11 | # echo "Removing console.logs from $file" 12 | # $(sed -i '/console.log/d' $file) 13 | # done 14 | 15 | # debugger_files=$(grep -lr 'debugger' js/*.js) 16 | # for file in $debugger_files; do 17 | # echo "Removing debuggers from $file" 18 | # $(sed -i '/debugger/d' $file) 19 | # done 20 | # 21 | # # search for localhost in utils file 22 | # localhost_file=$(grep -lr 'localhost' js/helpers/helpers.js) 23 | # for file in $localhost_file; do 24 | # echo "Removing localhost from $file" 25 | # $(sed -i '/localhost/d' $file) 26 | # done 27 | 28 | git add . 29 | 30 | echo $(git status) 31 | 32 | git commit --amend --no-edit 33 | 34 | npm run bab && node ./node_modules/eslint/bin/eslint.js js/ 35 | -------------------------------------------------------------------------------- /lib/models/contrastCredentials.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 8 | 9 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 10 | 11 | var _util = require('../util.js'); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var ContrastCredentials = function ContrastCredentials(options) { 16 | (0, _classCallCheck3.default)(this, ContrastCredentials); 17 | var apiKey = options.apiKey, 18 | orgUuid = options.orgUuid, 19 | teamServerUrl = options.teamServerUrl, 20 | serviceKey = options.serviceKey, 21 | profileEmail = options.profileEmail; 22 | 23 | this[_util.CONTRAST_API_KEY] = apiKey; 24 | this[_util.CONTRAST_ORG_UUID] = orgUuid; 25 | this[_util.TEAMSERVER_URL] = teamServerUrl; 26 | this[_util.CONTRAST_SERVICE_KEY] = serviceKey; 27 | this[_util.CONTRAST_USERNAME] = profileEmail; 28 | }; 29 | 30 | exports.default = ContrastCredentials; -------------------------------------------------------------------------------- /scripts/bab.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | DIR=$(pwd) 5 | SNAP_DIR="__snapshots__" 6 | 7 | echo "$(tput setaf 2) Running Babel, placing transpiled files into lib directory.$(tput sgr0)" 8 | babel ${DIR}/js --out-dir ${DIR}/lib 9 | echo "" 10 | 11 | echo "$(tput setaf 2) Running Mocha tests...$(tput sgr0)" 12 | node ./node_modules/nyc/bin/nyc.js node_modules/mocha/bin/mocha ${DIR}/test/spec/* 13 | echo "" 14 | 15 | echo "$(tput setaf 2) Creating a new __snapshots__ folder in project root.$(tput sgr0)" 16 | mkdir -p ${DIR}/${SNAP_DIR} 17 | mkdir -p ${DIR}/test/${SNAP_DIR} 18 | echo "" 19 | 20 | echo "$(tput setaf 2) Copying snapshot files from test dir to snapshot dir.$(tput sgr0)" 21 | cp -r ${DIR}/test/${SNAP_DIR}/* ${DIR}/${SNAP_DIR}/* 22 | echo "" 23 | 24 | echo "$(tput setaf 2) Running Snapshot tests...$(tput sgr0)" 25 | ./node_modules/mocha/bin/mocha test/snapshots/* 26 | echo "" 27 | 28 | echo "$(tput setaf 2) Copying Snapshots back to test directory $(tput sgr0)" 29 | cp -r ${DIR}/${SNAP_DIR} ${DIR}/test/${SNAP_DIR}/* 30 | 31 | echo "$(tput setaf 2) Cleaning up snapshot directory in project root. $(tput sgr0)" 32 | rm -rf ${DIR}/${SNAP_DIR} 33 | echo "" 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "https://github.com/Contrast-Security-OSS/contrast-chrome-extension", 3 | "scripts": { 4 | "test": "scripts/test.sh", 5 | "bab": "scripts/bab.sh", 6 | "lint": "eslint js/", 7 | "prettier": "prettier --config .prettierrc.json js/*", 8 | "build": "scripts/deploy.sh", 9 | "gulp": "gulp" 10 | }, 11 | "devDependencies": { 12 | "babel-cli": "^6.26.0", 13 | "babel-plugin-transform-async-to-generator": "^6.24.1", 14 | "babel-plugin-transform-runtime": "^6.23.0", 15 | "babel-preset-env": "^1.7.0", 16 | "btoa": "^1.2.1", 17 | "chai": "^4.1.2", 18 | "eslint": "^5.16.0", 19 | "gulp": "^3.9.1", 20 | "gulp-uglify": "^3.0.1", 21 | "gulp-uglify-es": "^1.0.4", 22 | "jest-webextension-mock": "^3.4.0", 23 | "jsdom": "^11.11.0", 24 | "mocha": "^6.1.4", 25 | "node-fetch": "^2.1.2", 26 | "nyc": "^12.0.2", 27 | "prettier": "1.14.2", 28 | "pump": "^3.0.0", 29 | "sinon": "^5.0.10", 30 | "sinon-chrome": "^2.3.2", 31 | "snap-shot-it": "^6.1.8" 32 | }, 33 | "jest": { 34 | "setupFiles": [ 35 | "./test/__setups__/chrome.js" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dist/js/models/VulnerableTab.js: -------------------------------------------------------------------------------- 1 | import{deDupeArray,murmur,isEmptyObject}from"../util.js";function VulnerableTabError(a,t,e){throw new Error(a,t,e)}function VulnerableTab(a,t,e=[]){this.traceIDs=e,this.path=a.split("?")[0],this.vulnTabId=murmur(this.path+"|"+t),this.appNameHash=murmur(t)}VulnerableTab.prototype.setTraceIDs=function(a){this.traceIDs=deDupeArray(this.traceIDs.concat(a))},VulnerableTab.prototype.storeTab=function(){return new Promise(async(a,t)=>{let e=await this.getApplicationTabs();e[this.appNameHash][this.vulnTabId]=this.traceIDs,chrome.storage.local.set(e,()=>{chrome.storage.local.get(this.appNameHash,e=>{e&&e[this.appNameHash]?a(e[this.appNameHash]):t(new VulnerableTabError("Error Storing Tab",this.vulnTabId,this.path))})})})},VulnerableTab.prototype.getApplicationTabs=function(){return new Promise(a=>{chrome.storage.local.get(this.appNameHash,t=>{t&&!isEmptyObject(t)||(t[this.appNameHash]={}),a(t)})})},VulnerableTab.prototype.getStoredTab=function(){return new Promise(a=>{chrome.storage.local.get(this.appNameHash,t=>{t&&t[this.appNameHash]?a(t[this.appNameHash]):a(null)})})},VulnerableTab.buildTabPath=function(a){const t=new URL(a);let e=t.pathname;return t.hash&&(e+=t.hash),e};export default VulnerableTab; -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Contrast", 4 | "description": "Allows the user to get information from Contrast conveniently in a Chrome extension", 5 | "version": "0.1", 6 | "browser_action": { 7 | "default_icon": "img/contrast-not-configured.png", 8 | "default_popup": "html/index.html" 9 | }, 10 | "background": { 11 | "page": "html/background.html", 12 | "persistent": true 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "http://*/*", 18 | "https://*/*" 19 | ], 20 | "js": [ 21 | "js/content-scripts/content-utils.js", 22 | "js/content-scripts/ContrastForms.js", 23 | "js/content-scripts/content-script.js" 24 | ] 25 | } 26 | ], 27 | "icons": { 28 | "16": "img/contrast16.png", 29 | "48": "img/contrast48.png", 30 | "128": "img/contrast128.png" 31 | }, 32 | "web_accessible_resources": [ 33 | "img/*.png", 34 | "test/*", 35 | "data/*" 36 | ], 37 | "permissions": [ 38 | "webRequest", 39 | "activeTab", 40 | "storage", 41 | "http://*/*", 42 | "https://*/*", 43 | "https://*.contrastsecurity.com/", 44 | "tabs", 45 | "background", 46 | "notifications" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Contrast", 4 | "description": "Allows the user to get information from Contrast conveniently in a Chrome extension", 5 | "version": "0.1", 6 | "browser_action": { 7 | "default_icon": "img/contrast-not-configured.png", 8 | "default_popup": "html/index.html" 9 | }, 10 | "background": { 11 | "page": "html/background.html", 12 | "persistent": true 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "http://*/*", 18 | "https://*/*" 19 | ], 20 | "js": [ 21 | "js/content-scripts/content-utils.js", 22 | "js/content-scripts/ContrastForms.js", 23 | "js/content-scripts/content-script.js" 24 | ] 25 | } 26 | ], 27 | "icons": { 28 | "16": "img/contrast16.png", 29 | "48": "img/contrast48.png", 30 | "128": "img/contrast128.png" 31 | }, 32 | "web_accessible_resources": [ 33 | "img/*.png", 34 | "test/*", 35 | "data/*" 36 | ], 37 | "permissions": [ 38 | "webRequest", 39 | "activeTab", 40 | "storage", 41 | "http://*/*", 42 | "https://*/*", 43 | "https://*.contrastsecurity.com/", 44 | "tabs", 45 | "background", 46 | "notifications" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /js/models/DomainStorage.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | import { 3 | CONNECTED_APP_DOMAINS, 4 | } from '../util.js'; 5 | 6 | class DomainStorage { 7 | constructor() { 8 | const domains = this._getDomainsFromStorage(); 9 | this.domains = ["http://localhost:*/*"].concat(domains); 10 | } 11 | 12 | _getDomainsFromStorage() { // eslint-disable-line 13 | const domains = window.localStorage.getItem(CONNECTED_APP_DOMAINS); 14 | if (!domains) { 15 | return []; 16 | } else if (typeof domains === 'string') { 17 | return JSON.parse(domains); 18 | } 19 | return domains; 20 | } 21 | 22 | addDomainsToStorage(requestDomains) { 23 | let domains = this._getDomainsFromStorage(); 24 | domains = domains.concat(requestDomains); 25 | this._setNewDomains(domains); 26 | } 27 | 28 | removeDomainsFromStorage(requestDomains) { 29 | let domains = this._getDomainsFromStorage(); 30 | domains = domains.filter(d => !requestDomains.includes(d)); 31 | this._setNewDomains(domains); 32 | } 33 | 34 | _setNewDomains(domains) { 35 | this.domains = domains; 36 | window.localStorage.setItem(CONNECTED_APP_DOMAINS, JSON.stringify(domains)); 37 | } 38 | } 39 | 40 | export default DomainStorage; 41 | -------------------------------------------------------------------------------- /dist/js/libraries/VulnerableApplicationLibrary.js: -------------------------------------------------------------------------------- 1 | class VulnerableApplicationLibrary{constructor(e){this.name=e.name||e.parsedLibName,this.vulnerabilities=e.vulnerabilities,this.vulnerabilitiesCount=e.vulnerabilities.length,this.version=e.version}lowConfidenceVulnerabilities(){return{name:this.name,confidenceIsCorrectLibrary:"LOW",vulnerabilitiesCount:this.vulnerabilitiesCount,vulnerabilities:this.vulnerabilities.map(e=>{let i={};return e.atOrAbove&&(i.atOrAbove=e.atOrAbove),e.atOrBelow&&(i.atOrBelow=e.atOrBelow),e.above&&(i.above=e.above),e.below&&(i.below=e.below),{title:e.identifiers.summary,link:e.info[0],severity:e.severity,versions:i}})}}highConfidenceVulnerability(){let e=this._isCorrectVersion(this.vulnerabilities,this.version);return e?{name:this.name,severity:e.severity,title:e.identifiers.summary,link:e.info[0],confidenceIsCorrectLibrary:"HIGH",vulnerabilities:this.vulnerabilities,vulnerabilitiesCount:this.vulnerabilitiesCount}:e}_isCorrectVersion(e,i){if(!e||!i)return!1;for(let r=0,t=e.length;r=i)return!0}else if(e&&r){if(tr)return!0}else{if(e&&t=i)return!0;if(r&&t>r)return!0}return!1}_parseVersionNumber(e){return e.split("-")[0].split(/[a-zA-Z]/)[0]}}export default VulnerableApplicationLibrary; -------------------------------------------------------------------------------- /dist/js/models/Application.js: -------------------------------------------------------------------------------- 1 | import{STORED_APPS_KEY,CONTRAST_GREEN,getHostFromUrl,isBlacklisted,updateTabBadge,updateExtensionIcon,isEmptyObject}from"../util.js";export default function Application(e,r){this[e]=r.app_id,this.id=r.app_id,this.name=r.name,this.domain=e,this.host=e}Application.retrieveApplicationFromStorage=function(e){return new Promise((r,t)=>{chrome.storage.local.get(STORED_APPS_KEY,i=>{chrome.runtime.lastError&&t(new Error("Error retrieving stored applications")),i&&i[STORED_APPS_KEY]||(i={[STORED_APPS_KEY]:[]});const n=new URL(e.url),o=getHostFromUrl(n),a=i[STORED_APPS_KEY].filter(e=>e.host===o)[0];if(a){if(a&&a.name){const e=document.getElementById("scan-libs-text");e&&(e.innerText=`Current Application:\n${a.name.trim()}`)}r(a)}else{if(isBlacklisted(e.url)||chrome.runtime.lastError){if(isBlacklisted(e.url)&&!chrome.runtime.lastError)try{updateExtensionIcon(e,1),updateTabBadge(e,"",CONTRAST_GREEN)}catch(e){t(new Error("Error updating tab badge"))}}else try{updateExtensionIcon(e,1),updateTabBadge(e,"")}catch(e){t(new Error("Error updating tab badge"))}r(null)}})})},Application.getStoredApp=function(e,r){if(!r)throw new Error("application must be defined");if(isEmptyObject(e))return;return(e[STORED_APPS_KEY]||e).filter(e=>e.id===r.app_id)[0]},Application.subDomainColonForUnderscore=function(e){return"object"==typeof e?this._subColonOrUnderscore(e.domain):this._subColonOrUnderscore(e)},Application._subColonOrUnderscore=function(e){return e.includes("_")?e.replace("_",":"):e.includes(":")?e.replace(":","_"):e}; -------------------------------------------------------------------------------- /test/spec/models/ContrastCredentials-spec.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | const { expect } = chai; 3 | const util = require('../../../lib/util.js'); 4 | const ContrastCredentials = require('../../../lib/models/ContrastCredentials.js').default; 5 | 6 | const { 7 | CONTRAST_USERNAME, 8 | CONTRAST_SERVICE_KEY, 9 | CONTRAST_API_KEY, 10 | CONTRAST_ORG_UUID, 11 | TEAMSERVER_URL, 12 | } = util; 13 | 14 | describe('test ContrastCredentials', function() { 15 | 16 | const correctObj = { 17 | [CONTRAST_USERNAME]: "userMcUserson", 18 | [CONTRAST_SERVICE_KEY]: "serviceKey123", 19 | [CONTRAST_API_KEY]: "apiKey123", 20 | [CONTRAST_ORG_UUID]: "820b994a-c848-4e50-9f9c-23b6305a8b24", 21 | [TEAMSERVER_URL]: "http://localhost:19090/Contrast/api", 22 | } 23 | 24 | it('returns an incorrect credential object', function() { 25 | let creds = new ContrastCredentials({ 26 | apiKey: "userMcUserson", 27 | orgUuid: "serviceKey123", 28 | teamServerUrl: "apiKey123", 29 | serviceKey: "820b994a-c848-4e50-9f9c-23b6305a8b24", 30 | profileEmail: "http://localhost:19090/Contrast/api", 31 | }); 32 | expect(creds).to.not.deep.equal(correctObj); 33 | }); 34 | 35 | it('returns a correct credential object', function() { 36 | let creds = new ContrastCredentials({ 37 | apiKey: "apiKey123", 38 | orgUuid: "820b994a-c848-4e50-9f9c-23b6305a8b24", 39 | teamServerUrl: "http://localhost:19090/Contrast/api", 40 | serviceKey: "serviceKey123", 41 | profileEmail: "userMcUserson", 42 | }); 43 | expect(creds).to.deep.equal(correctObj); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /wapp-service/index.js: -------------------------------------------------------------------------------- 1 | // const Wappalyzer = require('wappalyzer'); 2 | // const express = require('express'); 3 | // const app = express(); 4 | // 5 | // app.use(express.json()); 6 | // 7 | // app.use((req, res, next) => { 8 | // res.header('Access-Control-Allow-Origin', '*'); 9 | // res.header('Access-Control-Allow-Headers', 'Origin, Content-Type, Accept'); 10 | // res.header('Access-Control-Allow-Methods', 'GET'); 11 | // 12 | // next(); 13 | // }); 14 | // 15 | // app.get("/", (req, res) => { 16 | // const { site } = req.query; 17 | // const options = { 18 | // debug: true, 19 | // delay: 500, 20 | // maxDepth: 4, 21 | // maxUrls: 20, 22 | // maxWait: 10000, 23 | // recursive: true, 24 | // userAgent: 'Wappalyzer', 25 | // }; 26 | // 27 | // if (!site.includes("http://") && !site.includes("https://")) { 28 | // if (site.includes("localhost")) { 29 | // site = "http://" + site 30 | // } else { 31 | // site = "https://" + site 32 | // } 33 | // } 34 | // 35 | // const wappalyzer = new Wappalyzer(site, options); 36 | // wappalyzer.analyze() 37 | // .then(libraries => { 38 | // res.status(200).send({ 39 | // success: true, 40 | // message: "Success", 41 | // libraries, 42 | // }); 43 | // }) 44 | // .catch(error => { 45 | // res.status(422).send({ 46 | // success: false, 47 | // message: error.toString(), 48 | // libraries: null, 49 | // }); 50 | // }) 51 | // }); 52 | // 53 | // const port = process.env.WAPP_PORT || 5203; 54 | // app.listen(port, console.log("Wappalyzer service started on port " + port)); 55 | -------------------------------------------------------------------------------- /dist/js/queue.js: -------------------------------------------------------------------------------- 1 | import{isBlacklisted,removeLoadingBadge,deDupeArray}from"./util.js";import Vulnerability from"./models/Vulnerability.js";class Queue{constructor(){this.xhrRequests=[],this.gatheredForms=[],this.traceIDs=[],this.xhrReady=!1,this.formsReady=!1,this.isCredentialed=!1,this.tab=null,this.application=null,this.tabUrl="",this.executionCount=0}addXHRequests(t,e){this.xhrReady=e,this.xhrRequests=this.xhrRequests.concat(t)}addForms(t,e){this.formsReady=e,this.gatheredForms=this.gatheredForms.concat(t)}setTab(t){if(!t.url)throw new Error("Tab URL is falsey, received",t.url);this.tab=t,this.tabUrl=t.url}setApplication(t){this.application=t}setCredentialed(t){this.isCredentialed=t}_increaseExecutionCount(){this.executionCount+=1}resetExecutionCount(){this.executionCount=0}_highLightVulnerableForms(t){const e=t.map(t=>!!(t.traces&&t.traces.length>0)&&t.action).filter(Boolean);Vulnerability.highlightForms(this.tab,e)}_evaluateForms(){return Vulnerability.evaluateFormActions(this.gatheredForms,this.tab,this.application)}async executeQueue(t){const e=this.tabUrl||this.tab.url;if(isBlacklisted(e))return void removeLoadingBadge(this.tab);if(![this.xhrReady,this.formsReady,this.tab,this.tabUrl,this.isCredentialed,this.application].every(Boolean))return;await Vulnerability.removeVulnerabilitiesFromStorage(this.tab);const i=await this._evaluateForms();i&&i.length>0&&this._highLightVulnerableForms(i);let s=this.xhrRequests.concat([this.tabUrl]);s=(s=s.filter(t=>!isBlacklisted(t))).map(t=>new URL(t).pathname),Vulnerability.evaluateVulnerabilities(this.isCredentialed,this.tab,deDupeArray(s),this.application,i?i.map(t=>t.traces).flatten():[],t),this._increaseExecutionCount()}}export default Queue; -------------------------------------------------------------------------------- /dist/js/models/ConnectedDomain.js: -------------------------------------------------------------------------------- 1 | import{STORED_APPS_KEY,setElementDisplay}from"../util.js";import Application from"./Application.js";export default function ConnectedDomain(o,t){this.host=o,this.application=t}ConnectedDomain.prototype.connectDomain=function(){return this._addDomainToStorage()},ConnectedDomain.prototype._addDomainToStorage=function(){const{host:o,application:t}=this;return new Promise((e,r)=>{chrome.storage.local.get(STORED_APPS_KEY,n=>{if(chrome.storage.lastError)return r(new Error("Error retrieving stored apps"));n[STORED_APPS_KEY]||(n[STORED_APPS_KEY]=[]);const i=this._verifyDomainNotInUse(n[STORED_APPS_KEY],o);if(i.constructor===Error)return r(i);const a=new Application(o,t),c=n[STORED_APPS_KEY].concat(a),s=document.getElementById("application-table");chrome.storage.local.set({[STORED_APPS_KEY]:c},()=>{setElementDisplay(s,"none"),e(!chrome.storage.lastError)})})})},ConnectedDomain.prototype._verifyDomainNotInUse=function(o,t){if(o.length>0)for(let e=0,r=o.length;e{chrome.storage.local.get(STORED_APPS_KEY,e=>{const r=this._filterOutApp(e);chrome.storage.local.set({[STORED_APPS_KEY]:r},()=>{chrome.runtime.lastError&&t(new Error(chrome.runtime.lastError)),o(!chrome.runtime.lastError)})})})},ConnectedDomain.prototype._filterOutApp=function(o){return o[STORED_APPS_KEY].filter(o=>o.id!==this.application.id)}; -------------------------------------------------------------------------------- /lib/popup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _popupMethods = require("./popupMethods.js"); 4 | 5 | var _util = require("./util.js"); 6 | 7 | /*global 8 | chrome, 9 | document, 10 | */ 11 | 12 | // import { 13 | // renderVulnerableLibraries, 14 | // } from './libraries/showLibraries.js' 15 | document.addEventListener("DOMContentLoaded", function () { 16 | chrome.storage.local.get(_util.STORED_APPS_KEY, function (result) { 17 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 18 | if (!tabs[0]) return; 19 | 20 | var tab = tabs[0]; 21 | var url = new URL(tab.url); 22 | var host = (0, _util.getHostFromUrl)(url); 23 | var store = result[_util.STORED_APPS_KEY]; 24 | var app = store ? store.filter(function (a) { 25 | return a[host]; 26 | })[0] : store; 27 | 28 | if (app && !(0, _util.isEmptyObject)(app)) { 29 | (0, _util.getStoredCredentials)().then(function (items) { 30 | if ((0, _util.isCredentialed)(items)) { 31 | renderLoadingIcon(); 32 | (0, _popupMethods.getStorageVulnsAndRender)(items, app, tab); 33 | // renderVulnerableLibraries(tab, app) 34 | } else { 35 | throw new Error("Not Credentialed"); 36 | } 37 | }).catch(function (error) { 38 | return new Error(error); 39 | }); 40 | } else { 41 | var vulnsFound = document.getElementById("vulnerabilities-found-on-page"); 42 | (0, _util.setElementDisplay)(vulnsFound, "none"); 43 | (0, _popupMethods.hideLoadingIcon)(); 44 | } 45 | }); 46 | }); 47 | }, false); 48 | 49 | function renderLoadingIcon() { 50 | var loading = document.getElementById("vulns-loading"); 51 | loading.style.display = "block"; 52 | } -------------------------------------------------------------------------------- /js/popup.js: -------------------------------------------------------------------------------- 1 | /*global 2 | chrome, 3 | document, 4 | */ 5 | 6 | // import { 7 | // renderVulnerableLibraries, 8 | // } from './libraries/showLibraries.js' 9 | import { getStorageVulnsAndRender, hideLoadingIcon } from "./popupMethods.js"; 10 | import { 11 | STORED_APPS_KEY, 12 | getStoredCredentials, 13 | isCredentialed, 14 | getHostFromUrl, 15 | isEmptyObject, 16 | setElementDisplay 17 | } from "./util.js"; 18 | 19 | document.addEventListener( 20 | "DOMContentLoaded", 21 | () => { 22 | chrome.storage.local.get(STORED_APPS_KEY, result => { 23 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 24 | if (!tabs[0]) return; 25 | 26 | const tab = tabs[0]; 27 | const url = new URL(tab.url); 28 | const host = getHostFromUrl(url); 29 | const store = result[STORED_APPS_KEY]; 30 | const app = store ? store.filter(a => a[host])[0] : store; 31 | 32 | if (app && !isEmptyObject(app)) { 33 | getStoredCredentials() 34 | .then(items => { 35 | if (isCredentialed(items)) { 36 | renderLoadingIcon(); 37 | getStorageVulnsAndRender(items, app, tab); 38 | // renderVulnerableLibraries(tab, app) 39 | } else { 40 | throw new Error("Not Credentialed"); 41 | } 42 | }) 43 | .catch(error => new Error(error)); 44 | } else { 45 | const vulnsFound = document.getElementById( 46 | "vulnerabilities-found-on-page" 47 | ); 48 | setElementDisplay(vulnsFound, "none"); 49 | hideLoadingIcon(); 50 | } 51 | }); 52 | }); 53 | }, 54 | false 55 | ); 56 | 57 | function renderLoadingIcon() { 58 | const loading = document.getElementById("vulns-loading"); 59 | loading.style.display = "block"; 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contrast Security Chrome Extension 2 | 3 | [![Build Status](https://travis-ci.org/Contrast-Security-OSS/contrast-chrome-extension.svg?branch=master)](https://travis-ci.org/Contrast-Security-OSS/contrast-chrome-extension) 4 | 5 | ## Experimental Software 6 | 7 | Hi! Thanks for checking out the extension. Please note that this is pre-alpha software and it is under active development. There might be bugs. We may add new features and discard other features. Feel free to let us know what you like and what you don't like through Github Issues. Thanks! 8 | 9 | ## Local Setup and Installation 10 | 11 | 1. Clone this repo 12 | 2. If you want to use a local version of Contrast, add localhost to `VALID_TEAMSERVER_HOSTNAMES` in `util.js` 13 | 3. Navigate to chrome://extensions 14 | 4. Check the 'developer mode' checkbox in the top right corner 15 | 5. Click the 'load unpacked extension' button 16 | 6. Navigate to the cloned repo and select the top level directory 17 | 7. View chrome extension in top right corner of chrome 18 | 8. Navigate to `Organization Settings > API` in Teamserver and load configuration. 19 | 20 | ### Setup 21 | 22 | Install Mocha and Babel CLI for testing and eslint for linting. 23 | `npm install -g mocha eslint babel-cli` 24 | 25 | Run `./setup.sh`, it installs a Git Hook which runs tests before any new commits are pushed to this repo. 26 | 27 | ### Run Tests 28 | 29 | ``` 30 | npm run bab # transpile js files and run tests 31 | npm run test # run tests without transpiling 32 | ``` 33 | 34 | ### Travis 35 | 36 | We're running on Travis here: https://travis-ci.org/Contrast-Security-OSS/contrast-chrome-extension 37 | 38 | ### Linting 39 | 40 | - Linting is done by eslint (https://eslint.org/). 41 | - Travis build includes eslint task, so there should be no warnings for the build to succeed. 42 | - To run eslint on all files in the js folder of the project: `eslint js`. 43 | -------------------------------------------------------------------------------- /dist/js/libraries/Library.js: -------------------------------------------------------------------------------- 1 | class Library{constructor(r,t){this.GET_LIB_VERSION="GET_LIB_VERSION",this.tab=r,this.library=t,this.extractor=null}_setExtrator(r){this.extractor=r}_setLibraryVersion(r){this.library.version=r}createVersionedLibrary(){return this.library.extractors&&this.library.extractors.func?(this._setExtrator(this.library.extractors.func[0]),this._extractLibraryVersion()):new Promise(r=>r(this.library))}_extractLibraryVersion(){return new Promise((r,t)=>{const{library:e,tab:i}=this;this._executeExtractionScript().then(t=>{chrome.tabs.sendMessage(i.id,{action:this.GET_LIB_VERSION,library:e},t=>{t?(this._setLibraryVersion(t),r(e)):(this._setLibraryVersion(this._getVersionFromFileName(e.jsFileName)),r(e))})}).catch(r=>{t(r)})})}_getVersionFromFileName(r){const t=r.match(/\b\d+(?:\.\d+)*\b/);return t?t[0]:null}_executeExtractionScript(){return new Promise(r=>{const{extractor:t,library:e,tab:i}=this,s={code:this._generateScriptTags({extractor:t,library:e})};chrome.tabs.executeScript(i.id,s,t=>{r(!!t)})})}_generateScriptTags(){const{extractor:r}=this;if(!this.library.parsedLibName||!r)return null;const t=this.library.parsedLibName.replace("-","_");return`try {\n var script${t} = document.createElement('script');\n var scriptRes${t} = document.createElement('span');\n script${t}.innerHTML = \`${`\n try {\n var _c_res${t} = ${r};\n var __docRes${t} = document.getElementById('__script_res_${t}');\n __docRes${t}.innerText = _c_res${t};\n } catch (e) {\n // console.log(e) // error getting libraries\n }`}\`;\n const elId_${t} = '__script_res_${t}'\n const el_${t} = document.getElementById(elId_${t});\n if (!el_${t}) {\n scriptRes${t}.setAttribute('id', elId_${t});\n document.body.appendChild(scriptRes${t});\n document.body.appendChild(script${t});\n scriptRes${t}.style.display = 'none';\n }\n } catch (e) {}`}}export default Library; -------------------------------------------------------------------------------- /dist/js/content-scripts/ContrastForms.js: -------------------------------------------------------------------------------- 1 | "use strict";function ContrastForm(t){this.forms=t}function parentHasDisplayNone(t){for(;"BODY"!==t.tagName;){if("none"===t.style.display)return!0;t=t.parentNode}return!1}ContrastForm.MESSAGE_SENT=!1,ContrastForm._extractActionsFromForm=function(t){let r=[];for(let o=0;o0].every(Boolean)&&r.push(n.action)}return r},ContrastForm.collectFormActions=function(t){chrome.storage.local.get(STORED_APPS_KEY,r=>{if(chrome.runtime.lastError)return;if(!r||!r[STORED_APPS_KEY])return;const o=new URL(window.location.href),n=getHostFromUrl(o);if(!r[STORED_APPS_KEY].filter(t=>t.host===n)[0])return;const e=this._scrapeDOMForForms()||[];this.MESSAGE_SENT=!0,this._sendFormActionsToBackground(e,t)})},ContrastForm._collectMutatedForms=function(t,r,o){const n=this._getFormsFromMutations(t);n.length>0&&(ContrastForm.MESSAGE_SENT=!0,this._sendFormActionsToBackground(n,o),window.CONTRAST__REFRESHED=!1,r.disconnect())}.bind(ContrastForm),ContrastForm._getFormsFromMutations=function(t){let r=[];return t.map(t=>{let o;if((o="FORM"===t.target.tagName?t.target:t.target.getElementsByTagName("form"))&&o.length>0){let t=this._extractActionsFromForm(o);return r.concat(t)}return null}).filter(Boolean)},ContrastForm._scrapeDOMForForms=function(){let t=[],r=[],o=document.getElementsByTagName("form");for(let r=0;r0&&(r=r.concat(this._extractActionsFromForm(t))),r},ContrastForm._sendFormActionsToBackground=function(t,r){r({sender:GATHER_FORMS_ACTION,formActions:deDupeArray(t.flatten())})},ContrastForm.highlightForms=function(t){if(!t||0===t.length)return!1;const r=document.getElementsByTagName("form");for(let o=0;o2e3||0===e.length}Vulnerability.evaluateVulnerabilities=function(e,a,r,t,n,i){const o=generateTraceURLString(r);isTooLong(o,r)||t&&t.id&&getOrganizationVulnerabilityIds(o,t.id).then(e=>{if(!e||!e.traces)throw updateExtensionIcon(a,1),updateTabBadge(a,"",CONTRAST_GREEN),new Error("Error getting json from app trace ids");this.storeTraces(deDupeArray(e.traces.concat(n)),a,t,i)}).catch(e=>{updateExtensionIcon(a,1),updateTabBadge(a,"!",CONTRAST_RED),console.error("Error getting organization vulnerability ids",e)})},Vulnerability.evaluateFormActions=async function(e,a,r){if(!e||0===e.length)return[];const t=e.map(e=>new URL(e).pathname).map(e=>generateTraceURLString([e])).map(e=>getOrganizationVulnerabilityIds(e,r.id));return(await Promise.all(t)).map((a,r)=>(a.action=e[r],a))},Vulnerability.evaluateSingleURL=async function(e,a,r){const t=new URL(e).pathname,n=generateTraceURLString([t]),i=await getOrganizationVulnerabilityIds(n,r.id);i&&i.traces&&this.storeTraces(i.traces,a,r)},Vulnerability.highlightForms=function(e,a){chrome.tabs.sendMessage(e.id,{action:HIGHLIGHT_VULNERABLE_FORMS,formActions:a})},Vulnerability.storeTraces=async function(e,a,r,t=null){const n=VulnerableTab.buildTabPath(a.url),i=new VulnerableTab(n,r.name,e);let o=await i.getStoredTab();if(o&&o[i.vulnTabId]&&Array.isArray(o[i.vulnTabId])){const a=o[i.vulnTabId].concat(e);i.setTraceIDs(a)}else if(o&&o[i.vulnTabId]&&!Array.isArray(o[i.vulnTabId]))throw new Error("Vulnerabilities not stored properly, should have received array.");o=await i.storeTab();try{updateExtensionIcon(a,0),updateTabBadge(a,o[i.vulnTabId].length.toString(),CONTRAST_RED)}catch(e){}t&&t()},Vulnerability.removeVulnerabilitiesFromStorage=function(){return new Promise((e,a)=>{chrome.storage.local.remove(STORED_TRACES_KEY,()=>{chrome.runtime.lastError&&a(new Error(chrome.runtime.lastError)),e()})})};export default Vulnerability; -------------------------------------------------------------------------------- /lib/models/DomainStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _stringify = require('babel-runtime/core-js/json/stringify'); 8 | 9 | var _stringify2 = _interopRequireDefault(_stringify); 10 | 11 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 12 | 13 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 14 | 15 | var _createClass2 = require('babel-runtime/helpers/createClass'); 16 | 17 | var _createClass3 = _interopRequireDefault(_createClass2); 18 | 19 | var _util = require('../util.js'); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | var DomainStorage = function () { 24 | function DomainStorage() { 25 | (0, _classCallCheck3.default)(this, DomainStorage); 26 | 27 | var domains = this._getDomainsFromStorage(); 28 | this.domains = ["http://localhost:*/*"].concat(domains); 29 | } 30 | 31 | (0, _createClass3.default)(DomainStorage, [{ 32 | key: '_getDomainsFromStorage', 33 | value: function _getDomainsFromStorage() { 34 | // eslint-disable-line 35 | var domains = window.localStorage.getItem(_util.CONNECTED_APP_DOMAINS); 36 | if (!domains) { 37 | return []; 38 | } else if (typeof domains === 'string') { 39 | return JSON.parse(domains); 40 | } 41 | return domains; 42 | } 43 | }, { 44 | key: 'addDomainsToStorage', 45 | value: function addDomainsToStorage(requestDomains) { 46 | var domains = this._getDomainsFromStorage(); 47 | domains = domains.concat(requestDomains); 48 | this._setNewDomains(domains); 49 | } 50 | }, { 51 | key: 'removeDomainsFromStorage', 52 | value: function removeDomainsFromStorage(requestDomains) { 53 | var domains = this._getDomainsFromStorage(); 54 | domains = domains.filter(function (d) { 55 | return !requestDomains.includes(d); 56 | }); 57 | this._setNewDomains(domains); 58 | } 59 | }, { 60 | key: '_setNewDomains', 61 | value: function _setNewDomains(domains) { 62 | this.domains = domains; 63 | window.localStorage.setItem(_util.CONNECTED_APP_DOMAINS, (0, _stringify2.default)(domains)); 64 | } 65 | }]); 66 | return DomainStorage; 67 | }(); /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 68 | 69 | 70 | exports.default = DomainStorage; -------------------------------------------------------------------------------- /dist/js/popupMethods.js: -------------------------------------------------------------------------------- 1 | import{CONTRAST_ORG_UUID,TEAMSERVER_URL,SEVERITY_NOTE,SEVERITY_LOW,SEVERITY_MEDIUM,SEVERITY_HIGH,SEVERITY_CRITICAL,SEVERITY,SEVERITY_BACKGROUND_COLORS,SEVERITY_TEXT_COLORS,TRACES_REQUEST,getVulnerabilityTeamserverUrl,setElementDisplay,getVulnerabilityShort}from"./util.js";function hideLoadingIcon(){document.getElementById("vulns-loading").style.display="none"}function populateVulnerabilitySection(e,t,n,i){e.length>0&&getShortVulnerabilities(e,i.id).then(e=>{hideLoadingIcon(),e.map(e=>renderListItem(e,t,n,i))}).catch(new Error("Error rendering sorted traces into list items."))}function getShortVulnerabilities(e,t){return Promise.all(e.map(e=>getVulnerabilityShort(e,t))).then(e=>e.map(e=>e.trace).sort((e,t)=>SEVERITY[t.severity]-SEVERITY[e.severity])).catch(new Error("Error getting and rendering vulnerabilities"))}function renderListItem(e,t,n){if(!e)return;let i=document.getElementById("vulnerabilities-found-on-page-list"),a=document.createElement("li");switch(a.classList.add("no-border"),a.classList.add("vulnerability-li"),e.severity){case SEVERITY_NOTE:createBadge(SEVERITY_NOTE,a);break;case SEVERITY_LOW:createBadge(SEVERITY_LOW,a);break;case SEVERITY_MEDIUM:createBadge(SEVERITY_MEDIUM,a);break;case SEVERITY_HIGH:createBadge(SEVERITY_HIGH,a);break;case SEVERITY_CRITICAL:createBadge(SEVERITY_CRITICAL,a)}const r=document.createElement("a");r.classList.add("vulnerability-link"),r.classList.add("vulnerability-rule-name"),r.innerText=e.title,r.onclick=function(){chrome.tabs.create({url:getVulnerabilityTeamserverUrl(t,n,e.uuid),active:!1})},a.appendChild(r),i.appendChild(a)}function getStorageVulnsAndRender(e,t,n){const i=document.getElementById("no-vulnerabilities-found"),a=document.getElementById("vulnerabilities-found-on-page");chrome.runtime.sendMessage({action:TRACES_REQUEST,application:t,tab:n},n=>{n&&n.traces&&n.traces.length>0?(setElementDisplay(i,"none"),setElementDisplay(a,"block"),populateVulnerabilitySection(n.traces,e[TEAMSERVER_URL],e[CONTRAST_ORG_UUID],t)):(hideLoadingIcon(),setElementDisplay(i,"block"),setElementDisplay(a,"none"))})}const createBadge=(e,t)=>{let n=document.createElement("div");n.classList.add("parent-badge");let i=document.createElement("div");i.classList.add("child-badge"),i.innerText=e,i.style.color=SEVERITY_TEXT_COLORS[e],n.style.backgroundColor=SEVERITY_BACKGROUND_COLORS[e],n.appendChild(i),t.appendChild(n)};export{hideLoadingIcon,populateVulnerabilitySection,renderListItem,getStorageVulnsAndRender,getShortVulnerabilities}; -------------------------------------------------------------------------------- /js/models/VulnerableTab.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | import { 3 | deDupeArray, 4 | murmur, 5 | isEmptyObject, 6 | } from '../util.js'; 7 | 8 | function VulnerableTabError(message, vulnTabId, vulnTabUrl) { 9 | throw new Error(message, vulnTabId, vulnTabUrl); 10 | } 11 | 12 | function VulnerableTab(path, applicationName, traces = []) { 13 | this.traceIDs = traces; 14 | this.path = path.split("?")[0]; 15 | this.vulnTabId = murmur(this.path + "|" + applicationName); 16 | this.appNameHash = murmur(applicationName); 17 | } 18 | 19 | VulnerableTab.prototype.setTraceIDs = function(traceIDs) { 20 | this.traceIDs = deDupeArray(this.traceIDs.concat(traceIDs)); 21 | } 22 | 23 | VulnerableTab.prototype.storeTab = function() { 24 | return new Promise(async(resolve, reject) => { 25 | 26 | let appTabs = await this.getApplicationTabs(); 27 | appTabs[this.appNameHash][this.vulnTabId] = this.traceIDs; 28 | 29 | chrome.storage.local.set(appTabs, () => { 30 | chrome.storage.local.get(this.appNameHash, (storedTab) => { 31 | if (storedTab && storedTab[this.appNameHash]) { 32 | resolve(storedTab[this.appNameHash]); 33 | } else { 34 | reject(new VulnerableTabError("Error Storing Tab", this.vulnTabId, this.path)); 35 | } 36 | }); 37 | }); 38 | }); 39 | } 40 | 41 | VulnerableTab.prototype.getApplicationTabs = function() { 42 | return new Promise((resolve) => { 43 | chrome.storage.local.get(this.appNameHash, (appTabs) => { 44 | 45 | // NOTE: if an application has just been added, appTabs will be empty obj 46 | // Add appNameHash key with val as empty object for storing vulnTabIds 47 | if (!appTabs || isEmptyObject(appTabs)) { 48 | appTabs[this.appNameHash] = {}; 49 | } 50 | 51 | resolve(appTabs) 52 | }); 53 | }); 54 | } 55 | 56 | VulnerableTab.prototype.getStoredTab = function() { 57 | return new Promise((resolve) => { 58 | chrome.storage.local.get(this.appNameHash, (storedTabs) => { 59 | if (storedTabs && storedTabs[this.appNameHash]) { 60 | resolve(storedTabs[this.appNameHash]); 61 | } else { 62 | resolve(null); 63 | } 64 | }); 65 | }); 66 | } 67 | 68 | VulnerableTab.buildTabPath = function(tabUrl) { 69 | const url = (new URL(tabUrl)); 70 | let path = url.pathname; 71 | if (url.hash) { 72 | path += url.hash; 73 | } 74 | return path; 75 | } 76 | 77 | export default VulnerableTab; 78 | -------------------------------------------------------------------------------- /dist/js/libraries/ApplicationLibrary.js: -------------------------------------------------------------------------------- 1 | import{GATHER_SCRIPTS,CONTRAST__STORED_APP_LIBS,isEmptyObject}from"../util.js";import Library from"./Library.js";import VulnerableApplicationLibrary from"./VulnerableApplicationLibrary.js";class ApplicationLibrary{constructor(i,e){this.tab=i,this.application=e,this.libraries=[],this.STORED_APP_LIBS_ID="APP_LIBS__ID_"+e.domain}_setCurrentLibs(i){i&&Array.isArray(i)&&(this.libraries=i.filter(Boolean))}getApplicationLibraries(){return new Promise((i,e)=>{const{tab:r}=this;chrome.tabs.sendMessage(r.id,{action:GATHER_SCRIPTS,tab:r},t=>{if(!t)return void e(new Error("No Response to GATHER_SCRIPTS"));const{sharedLibraries:a}=t;let n;try{n=a.map(i=>new Library(r,i).createVersionedLibrary())}catch(i){return}Promise.all(n).then(e=>{const r=e.map(i=>{let e=new VulnerableApplicationLibrary(i);return i&&i.vulnerabilities&&i.version?e.highConfidenceVulnerability():e.lowConfidenceVulnerabilities()}).filter(Boolean);i(r)}).catch(i=>{})})})}addNewApplicationLibraries(i){return new Promise(async e=>{const{STORED_APP_LIBS_ID:r}=this,t=await this._getStoredApplicationLibraries(),a=t[CONTRAST__STORED_APP_LIBS]?t[CONTRAST__STORED_APP_LIBS][r]:null;if(this._setCurrentLibs(a),!t||isEmptyObject(t))t[CONTRAST__STORED_APP_LIBS]={},t[CONTRAST__STORED_APP_LIBS][r]=i;else if(!isEmptyObject(t[CONTRAST__STORED_APP_LIBS])&&a&&Array.isArray(a)){const n=this._dedupeLibs(i);if(0===n.length)return void e(null);const s=a.concat(n);t[CONTRAST__STORED_APP_LIBS][r]=s}else t[CONTRAST__STORED_APP_LIBS][r]=i;chrome.storage.local.set(t,function(){chrome.storage.local.get(CONTRAST__STORED_APP_LIBS,function(i){e(i[CONTRAST__STORED_APP_LIBS][r])})})})}_getStoredApplicationLibraries(){return new Promise((i,e)=>{chrome.storage.local.get(CONTRAST__STORED_APP_LIBS,r=>{!r||isEmptyObject(r)?i({}):i(r),e(new Error("Stored Libs are",typeof r))})})}_dedupeLibs(i){return i.filter(i=>{return!this.libraries.filter(e=>e.name===i.name&&i.vulnerabilitiesCount>1?e.vulnerabilities.length===i.vulnerabilities.length||(i.vulnerabilities=i.vulnerabilities.filter(i=>e.vulnerabilities.filter(e=>e.title!==i.title)),0===i.vulnerabilities.length):e.name===i.name)[0]})}removeAndSetupApplicationLibraries(){if(!this.application||!this.STORED_APP_LIBS_ID)throw new Error("Application and STORED_APP_LIBS_ID are not set.");return chrome.storage.local.remove(CONTRAST__STORED_APP_LIBS),this._setupApplicationLibraries()}async _setupApplicationLibraries(){const i=await this.getApplicationLibraries();return i&&0!==i.length?this.addNewApplicationLibraries(i):null}}export default ApplicationLibrary; -------------------------------------------------------------------------------- /dist/js/libraries/showLibraries.js: -------------------------------------------------------------------------------- 1 | import{SEVERITY,SEVERITY_LOW,SEVERITY_MEDIUM,SEVERITY_HIGH,CONTRAST__STORED_APP_LIBS,SEVERITY_BACKGROUND_COLORS,SEVERITY_TEXT_COLORS,isEmptyObject,capitalize}from"../util.js";import Application from"../models/Application.js";const getLibrariesFromStorage=(e,t)=>new Promise((e,i)=>{const a="APP_LIBS__ID_"+t.domain;chrome.storage.local.get(CONTRAST__STORED_APP_LIBS,t=>{if(isEmptyObject(t))e(null);else{const i=t[CONTRAST__STORED_APP_LIBS][a];e(i)}i(new Error("result was",typeof t))})}),_getTabAndApplication=()=>new Promise((e,t)=>{chrome.tabs.query({active:!0,currentWindow:!0},async i=>{const a=i[0];if(!a)return void t(new Error("Tab is null"));const l=await Application.retrieveApplicationFromStorage(a);e({tab:a,application:l})})}),renderVulnerableLibraries=async(e,t)=>{if(!e||!t){const i=await _getTabAndApplication();e=i.tab,t=i.application}let i=await getLibrariesFromStorage(0,t);if(!i||0===i.length)return;const a=document.getElementById("libs-vulnerabilities-found-on-page"),l=document.getElementById("libs-vulnerabilities-found-on-page-list");let r=[];for(let e=0,t=(i=i.sort((e,t)=>!e.severity&&t.severity?1:e.severity&&!t.severity?-1:e.severity||t.severity?SEVERITY[t.severity.titleize()]-SEVERITY[e.severity.titleize()]:0)).length;e{let t=e.title;return t||("string"!=typeof(t=e.identifiers)?t.summary:t)},createBadge=(e,t)=>{let i=document.createElement("div");i.classList.add("parent-badge");let a=document.createElement("div");a.classList.add("child-badge"),a.innerText=e,a.style.color=SEVERITY_TEXT_COLORS[e],i.style.backgroundColor=SEVERITY_BACKGROUND_COLORS[e],i.appendChild(a),t.appendChild(i)},_createVulnerabilityListItem=(e,t,i)=>{let{name:a,severity:l,title:r,link:n}=i;a||(a=(a=t).titleize());let s=document.createElement("li");switch(s.classList.add("list-group-item"),s.classList.add("no-border"),s.classList.add("vulnerability-li"),l.toLowerCase()){case SEVERITY_LOW.toLowerCase():createBadge(SEVERITY_LOW,s);break;case SEVERITY_MEDIUM.toLowerCase():createBadge(SEVERITY_MEDIUM,s);break;case SEVERITY_HIGH.toLowerCase():createBadge(SEVERITY_HIGH,s)}a=a.replace("Jquery","JQuery");let o=document.createElement("a");o.classList.add("vulnerability-link"),o.classList.add("vulnerability-rule-name"),o.innerText=a+": "+capitalize(r.trim()),o.onclick=function(){chrome.tabs.create({url:n,active:!1})},s.appendChild(o),e.appendChild(s)};export{renderVulnerableLibraries}; -------------------------------------------------------------------------------- /requirements.md: -------------------------------------------------------------------------------- 1 | # Contrast Chrome Extension Requirements 2 | 3 | * Gather traces on a given page. 4 | - XHR requests to external resources and get/post requests to API 5 | - Tab URL 6 | - Form actions on page 7 | 8 | * Show a user the number of traces 9 | * Show a user severity and description of trace 10 | 11 | ### Get list of application in an organization 12 | * ex. Send `http://localhost:19080/Contrast/api/ng/{orgUuid}/applications/name"` 13 | Expect back: 14 | ```js 15 | [ 16 | { 17 | "name": "bhima", 18 | "app_id": "c05a5016-6440-40ad-b91f-33610515d130" 19 | }, 20 | { 21 | "name": "juice-shop", 22 | "app_id": "d1829729-83b5-4050-bff2-71bc4bfb2571" 23 | }, 24 | { 25 | "name": "LegismeApi", 26 | "app_id": "a51c4303-7a6d-4578-9dde-d89a37fae50c" 27 | }, 28 | { 29 | "name": "webgoat-server", 30 | "app_id": "d834de81-3069-499a-9ddc-eeb30375fdbf" 31 | }, 32 | ]; 33 | ``` 34 | 35 | ### Send URL, or a list of URLs to Teamserver, expect back an array of trace-ids. 36 | `/ng/{orgUuid}/traces/{appId}/ids` 37 | * ex. Send the following URLs: 38 | ```js 39 | [ 40 | "http://localhost:8080/WebGoat/start.mvc#lesson/SqlInjection.lesson/6", 41 | "http://localhost:8080/WebGoat/SqlInjection/attack5a", 42 | "http://localhost:8080/WebGoat/SqlInjection/attack5b" 43 | ] 44 | ``` 45 | Expect back: 46 | ```js 47 | [ 48 | "8XTN-KCOY-0ASI-4PTG", 49 | "O2ZH-E7WW-MXYT-4DQ2", 50 | "LO1G-Z49Q-JJGP-1G4H", 51 | "RCGK-RQR3-I1VM-34AO" 52 | ] 53 | ``` 54 | 55 | __URLs with parameters in the URL should also return traces.__ 56 | ex: Send base64 encoded URL: `http://localhost:4000/allocations/5` 57 | Respond with a list including trace ID, even though teamserver stores the Trace Route as `/allocations/:userId`. 58 | ```js 59 | [ 60 | "HJH9-YWFW-TEYA-469K", 61 | ] 62 | ``` 63 | 64 | ### Send a Trace ID, return a Short Trace for showing info to a user. 65 | * ex. Send the following 66 | ```js 67 | `/Contrast/api/ng/{org_uuid}/traces/{app_uuid}/ids?urls={base64_url,optionally_another_base64_url}` 68 | `/Contrast/api/ng/04bfd6c5-b24e-4610-b8b9-bcbde09f8e15/traces/d834de81-3069-499a-9ddc-eeb30375fdbf/ids?urls=aHR0cDovL2xvY2FsaG9zdDo4MDgwL1dlYkdvYXQvc3RhcnQubXZjI2xlc3Nvbi9TcWxJbmplY3Rpb24ubGVzc29uLzY=` 69 | ``` 70 | 71 | Should return: 72 | ```js 73 | [ 74 | { 75 | "success": true, 76 | "messages": [ 77 | "Vulnerability short loaded successfully" 78 | ], 79 | "trace": { 80 | "app_id": "d834de81-3069-499a-9ddc-eeb30375fdbf", 81 | "first_time_seen": 1526912769230, 82 | "impact": "High", 83 | "license": "Licensed", 84 | "likelihood": "High", 85 | "ruleName": "SQL Injection", 86 | "severity": "Critical", 87 | "status": "Reported", 88 | "uuid": "O2ZH-E7WW-MXYT-4DQ2", 89 | "visible": true 90 | }, 91 | "links": [] 92 | } 93 | ] 94 | ``` 95 | -------------------------------------------------------------------------------- /test/snapshots/index.test.js: -------------------------------------------------------------------------------- 1 | const chrome = require("sinon-chrome/extensions"); 2 | global.chrome = chrome; 3 | 4 | const url = require("url"); 5 | global.URL = url.URL; 6 | 7 | const jsdom = require("jsdom"); 8 | const { JSDOM } = jsdom; 9 | 10 | const fs = require("fs"); 11 | const snapshot = require("snap-shot-it"); 12 | const sinon = require("sinon"); 13 | const chai = require("chai"); 14 | const { expect } = chai; 15 | 16 | const util = require("../../lib/util.js"); 17 | const { getOrgApplications } = util; 18 | const testData = require("../testData.js"); 19 | const ApplicationTable = require("../../lib/models/ApplicationTable.js") 20 | .default; 21 | 22 | const file = fs.readFileSync("html/index.html"); 23 | const html = file.toString(); 24 | global.window = new JSDOM(html, { url: "http://localhost" }).window; 25 | global.document = global.window.document; 26 | 27 | const Config = require("../../lib/models/Config.js").default; 28 | 29 | describe("tests the popup index.html file for changes", () => { 30 | const config = new Config( 31 | { id: 1, url: "http://localhost:8080/WebGoat" }, 32 | "http://localhost:8080/WebGoat", 33 | true, 34 | {}, 35 | true 36 | ); 37 | 38 | before(() => { 39 | global.window = new JSDOM(html, { url: "http://localhost" }).window; 40 | global.document = global.window.document; 41 | }); 42 | 43 | it("takes a snapshot of the base index.html file", async () => { 44 | snapshot(global.document.querySelector("html").innerHTML); 45 | }); 46 | 47 | it("takes a snapshot of the notContrastNotConfigured index.html file", async () => { 48 | config.popupScreen(0); 49 | snapshot(global.document.querySelector("html").innerHTML); 50 | }); 51 | 52 | it("takes a snapshot of the contrastNotConfigured index.html file", async () => { 53 | config.popupScreen(1); 54 | snapshot(global.document.querySelector("html").innerHTML); 55 | }); 56 | 57 | it("takes a snapshot of the contrastYourAccountNotConfigured index.html file", async () => { 58 | config.popupScreen(2); 59 | snapshot(global.document.querySelector("html").innerHTML); 60 | }); 61 | 62 | it("takes a snapshot of the contrastYourAccountConfigured index.html file", async () => { 63 | config.popupScreen(3); 64 | snapshot(global.document.querySelector("html").innerHTML); 65 | }); 66 | 67 | it("renders a table of applications", async () => { 68 | const appTable = new ApplicationTable( 69 | new URL("http://localhost:8080/WebGoat") 70 | ); 71 | 72 | const isContrastTeamserverMock = sinon.stub(util, "isContrastTeamserver"); 73 | isContrastTeamserverMock.returns(true); 74 | 75 | let app = testData.application; 76 | app.connectedAlready = true; 77 | 78 | appTable._showContrastApplications(testData.storedApps); 79 | appTable.createAppTableRow(app, testData.storedApps); 80 | snapshot( 81 | global.document.querySelector("#application-table-container-section") 82 | .innerHTML 83 | ); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /js/libraries/VulnerableApplicationLibrary.js: -------------------------------------------------------------------------------- 1 | class VulnerableApplicationLibrary { 2 | constructor(vulnerableLibrary) { 3 | this.name = vulnerableLibrary.name || vulnerableLibrary.parsedLibName; 4 | this.vulnerabilities = vulnerableLibrary.vulnerabilities; 5 | this.vulnerabilitiesCount = vulnerableLibrary.vulnerabilities.length; 6 | this.version = vulnerableLibrary.version; 7 | } 8 | 9 | lowConfidenceVulnerabilities() { 10 | return { 11 | name: this.name, 12 | confidenceIsCorrectLibrary: 'LOW', 13 | vulnerabilitiesCount: this.vulnerabilitiesCount, 14 | vulnerabilities: this.vulnerabilities.map(v => { 15 | let versions = {}; 16 | v.atOrAbove ? versions.atOrAbove = v.atOrAbove : null; 17 | v.atOrBelow ? versions.atOrBelow = v.atOrBelow : null; 18 | v.above ? versions.above = v.above : null; 19 | v.below ? versions.below = v.below : null; 20 | return { 21 | title: v.identifiers.summary, 22 | link: v.info[0], 23 | severity: v.severity, 24 | versions, 25 | }; 26 | }), 27 | }; 28 | } 29 | 30 | highConfidenceVulnerability() { 31 | let vulnObj = this._isCorrectVersion(this.vulnerabilities, this.version); 32 | if (!vulnObj) return vulnObj; 33 | return { 34 | name: this.name, 35 | severity: vulnObj.severity, 36 | title: vulnObj.identifiers.summary, 37 | link: vulnObj.info[0], 38 | confidenceIsCorrectLibrary: 'HIGH', 39 | vulnerabilities: this.vulnerabilities, 40 | vulnerabilitiesCount: this.vulnerabilitiesCount, 41 | } 42 | } 43 | 44 | _isCorrectVersion(vulnerabilityObjects, libVersion) { 45 | if (!vulnerabilityObjects || !libVersion) return false; 46 | 47 | for (let i = 0, len = vulnerabilityObjects.length; i < len; i++) { 48 | let vuln = vulnerabilityObjects[i]; 49 | let { below, atOrAbove, above } = vuln; 50 | if (below) { 51 | below = this._parseVersionNumber(below); 52 | } 53 | if (atOrAbove) { 54 | atOrAbove = this._parseVersionNumber(atOrAbove); 55 | } 56 | if (above) { 57 | above = this._parseVersionNumber(above); 58 | } 59 | if (this._hasVulnerableVersion(below, atOrAbove, above, libVersion)) { 60 | return vuln; 61 | } 62 | 63 | // get script obj that has matching bowername 64 | // compare script vuln version to vulnObj versions 65 | // true if is correct version 66 | } 67 | return null; 68 | } 69 | 70 | _hasVulnerableVersion(below, atOrAbove, above, libVersion) { 71 | if (below && atOrAbove) { 72 | if (libVersion < below && libVersion >= atOrAbove) { 73 | return true; 74 | } 75 | } else if (below && above) { 76 | if (libVersion < below && libVersion > above) { 77 | return true; 78 | } 79 | } else if (below && libVersion < below) { 80 | return true; 81 | } else if (atOrAbove && libVersion >= atOrAbove) { 82 | return true; 83 | } else if (above && libVersion > above) { 84 | return true; 85 | } 86 | return false; 87 | } 88 | 89 | _parseVersionNumber(string) { 90 | return string.split("-")[0].split(/[a-zA-Z]/)[0]; 91 | } 92 | } 93 | 94 | export default VulnerableApplicationLibrary; 95 | -------------------------------------------------------------------------------- /test/spec/settings-spec.js: -------------------------------------------------------------------------------- 1 | const chrome = require('sinon-chrome/extensions'); 2 | const jsdom = require("jsdom"); 3 | const testData = require("../testData") 4 | const sinon = require("sinon"); 5 | const chai = require("chai"); 6 | const { assert, expect } = chai; 7 | const Helpers = require('../../lib/util.js'); 8 | const { 9 | CONTRAST_USERNAME, 10 | CONTRAST_SERVICE_KEY, 11 | CONTRAST_API_KEY, 12 | CONTRAST_ORG_UUID, 13 | TEAMSERVER_URL, 14 | processTeamserverUrl, 15 | } = Helpers; 16 | 17 | const { JSDOM } = jsdom; 18 | 19 | describe('setting the initial credentials for the extension from teamserver', () => { 20 | 21 | global.document = (new JSDOM( 22 | `` 23 | )).window.document; 24 | 25 | const dummyElement = document.createElement('button') 26 | const dummyUsername = document.createElement('input') 27 | const dummyServiceKey = document.createElement('input') 28 | const dummyApiKey = document.createElement('input') 29 | const dummyOrgUuid = document.createElement('input') 30 | const dummyTeamserverUrl = document.createElement('input') 31 | 32 | dummyUsername.value = "contrast_admin" 33 | dummyServiceKey.value = "demo" 34 | dummyApiKey.value = "demo" 35 | dummyOrgUuid.value = "04bfd6c5-b24e-4610-b8b9-bcbde09f8e15" 36 | dummyTeamserverUrl.value = "localhost:19080" 37 | 38 | let contrastCredentials; 39 | 40 | beforeEach(() => { 41 | global.chrome = chrome; 42 | contrastCredentials = { 43 | [CONTRAST_USERNAME]: dummyUsername.value, 44 | [CONTRAST_SERVICE_KEY]: dummyServiceKey.value, 45 | [CONTRAST_API_KEY]: dummyApiKey.value, 46 | [CONTRAST_ORG_UUID]: dummyOrgUuid.value, 47 | [TEAMSERVER_URL]: processTeamserverUrl(dummyTeamserverUrl.value), 48 | } 49 | 50 | dummyElement.click = function() { 51 | chrome.storage.local.get.yields(contrastCredentials); 52 | } 53 | chrome.storage.local.clear() 54 | }); 55 | 56 | it('fills in empty credentials', function() { 57 | const username = dummyUsername.value.trim(); 58 | const serviceKey = dummyServiceKey.value.trim(); 59 | const apiKey = dummyApiKey.value.trim(); 60 | const orgUuid = dummyOrgUuid.value.trim(); 61 | const teamserverUrl = processTeamserverUrl(dummyTeamserverUrl.value.trim()); 62 | 63 | expect(username).equal(dummyUsername.value); 64 | expect(serviceKey).equal(dummyServiceKey.value); 65 | expect(apiKey).equal(dummyApiKey.value); 66 | expect(orgUuid).equal(dummyOrgUuid.value); 67 | expect(teamserverUrl).equal("https://localhost:19080/Contrast/api"); 68 | 69 | chrome.storage.local.get.yields({}) 70 | chrome.storage.local.get([ 71 | CONTRAST_USERNAME, 72 | CONTRAST_SERVICE_KEY, 73 | CONTRAST_API_KEY, 74 | CONTRAST_ORG_UUID, 75 | TEAMSERVER_URL, 76 | ], (items) => { 77 | expect(Object.values(items).length).equal(0); 78 | }); 79 | 80 | dummyElement.click(); 81 | 82 | chrome.storage.local.get([ 83 | CONTRAST_USERNAME, 84 | CONTRAST_SERVICE_KEY, 85 | CONTRAST_API_KEY, 86 | CONTRAST_ORG_UUID, 87 | TEAMSERVER_URL, 88 | ], (items) => { 89 | expect(Object.values(items).length).equal(5); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/md5.min.js: -------------------------------------------------------------------------------- 1 | !function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u,c;n[t>>5]|=128<>>9<<4)]=t;var f=1732584193,i=-271733879,a=-1732584194,h=271733878;for(r=0;r>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<>>4&15)+e.charAt(15&t);return o}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u,c;n[t>>5]|=128<>>9<<4)]=t;var f=1732584193,i=-271733879,a=-1732584194,h=271733878;for(r=0;r>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<>>4&15)+e.charAt(15&t);return o}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16e[t])[0]}function renderFailureMessage(e,t){const n=document.getElementById("configure-extension-button"),o=document.getElementById("config-failure"),i=document.getElementById("config-failure-message");e&&setElementText(i,e.toString()),changeElementVisibility(o),setElementDisplay(n,"none"),hideElementAfterTimeout(o,()=>{n.removeAttribute("disabled"),setElementDisplay(n,"block")},t)}ApplicationTable.prototype.renderApplicationsMenu=async function(){if(document.getElementsByTagName("tr").length<2){const e=await getOrgApplications();if(!e||e instanceof Error)return void renderFailureMessage("Error Getting Applications. Make sure your credentials are correct.");const t=await this._getStoredApplications(),n=this._filterApplications(t,e.applications);t&&n.forEach(e=>this.createAppTableRow(e,t))}},ApplicationTable.prototype.renderActivityFeed=async function(){this.tableContainer.classList.remove("collapsed");const e=await this._getStoredApplications();if(e){if(_appIsConfigured(e,getHostFromUrl(this.url))){const e=document.getElementById("application-table-container-section");setElementDisplay(e,"none")}else this._showContrastApplications(e)}},ApplicationTable.prototype._getStoredApplications=function(){return new Promise(e=>{chrome.storage.local.get(STORED_APPS_KEY,t=>chrome.runtime.lastError?e(null):e(t))})},ApplicationTable.prototype._showContrastApplications=function(e){const t=document.getElementById("vulnerabilities-section");setElementDisplay(t,"none");const n=document.getElementById("vulns-header-text"),o=n.parentElement.parentElement;setElementText(n,"Connect Applications"),n.style.fontSize="4.5vw",o.style.border="none",document.getElementById("configured-footer").style.border="none",getOrgApplications().then(t=>{if(!t||t instanceof Error)return void renderFailureMessage("Error Getting Applications. Make sure your credentials are correct.",5e3);const n=this._filterApplications(e,t.applications);this.createTableRows(n,e)}).catch(()=>{renderFailureMessage("Error Getting Applications. Make sure your credentials are correct.",5e3)})},ApplicationTable.prototype.createTableRows=function(e,t){e.forEach(e=>this.createAppTableRow(e,t))},ApplicationTable.prototype._filterApplications=function(e,t){if(e[STORED_APPS_KEY]&&!isContrastTeamserver(this.url.href)){const n=e[STORED_APPS_KEY].map(e=>e.id).flatten();return t.map(e=>(e.connectedAlready=n.includes(e.app_id),e))}return t},ApplicationTable.prototype.createAppTableRow=function(e,t){if(!e||!e.name)return;const n=new TableRow(e,this.url,this.table.tBodies[0]);if(n.appendChildren(),isContrastTeamserver(this.url.href))if(t){const o=Application.getStoredApp(t,e);setElementText(n.nameTD,e.name),o&&(n.setHost(o.host),n.renderDisconnect(t,o))}else chrome.storage.local.get(STORED_APPS_KEY,t=>{if(chrome.runtime.lastError)return;t&&t[STORED_APPS_KEY]||(t={[STORED_APPS_KEY]:[]});const o=Application.getStoredApp(t,e);setElementText(n.nameTD,e.name),o&&(n.setHost(o.host),n.renderDisconnect(t,o))});else if(e.connectedAlready){const o=Application.getStoredApp(t,e);setElementText(n.nameTD,e.name),n.setHost(o.host),n.renderDisconnect(t,o)}else n.setHost(getHostFromUrl(this.url)),n.createConnectButton()}; -------------------------------------------------------------------------------- /dist/js/models/PopupTableRow.js: -------------------------------------------------------------------------------- 1 | import{CONTRAST_RED,CONTRAST_GREEN,setElementText,setElementDisplay,changeElementVisibility,APPLICATION_CONNECTED}from"../util.js";import Application from"./Application.js";import ConnectedDomain from"./ConnectedDomain.js";const HOST_SPAN_CLASS="app-host-span",CONNECT_BUTTON_TEXT="Connect",CONNECT_SUCCESS_MESSAGE="Successfully connected. Please reload the page.",CONNECT_FAILURE_MESSAGE="Error connecting. Try refreshing the page.",DISCONNECT_FAILURE_MESSAGE="Error Disconnecting",DISCONNECT_BUTTON_TEXT="Disconnect",CONTRAST_BUTTON_CLASS="btn btn-primary btn-xs btn-contrast-plugin btn-connect",CONTRAST_BUTTON_DISCONNECT_CLASS="btn btn-primary btn-xs btn-contrast-plugin btn-disconnect";export default function TableRow(t,e,n){this.application=t,this.url=e,this.table=n,this.host="",this.row=document.createElement("tr"),this.nameTD=document.createElement("td"),this.buttonTD=document.createElement("td")}TableRow.prototype.setHost=function(t){this.host=t},TableRow.prototype.appendChildren=function(){this.table.appendChild(this.row),this.row.appendChild(this.nameTD),this.row.appendChild(this.buttonTD)},TableRow.prototype.createConnectButton=function(){const t=this.buttonTD,e=document.createElement("button");e.setAttribute("class",`${CONTRAST_BUTTON_CLASS} domainBtn`),t.appendChild(e),setElementText(e,"Connect"),setElementText(this.nameTD,this.application.name.titleize()),e.addEventListener("click",()=>{new ConnectedDomain(this.host,this.application).connectDomain().then(t=>this._showMessage(t,!0)).catch(t=>this._handleConnectError(t))})},TableRow.prototype.renderDisconnect=function(t,e){const n=document.createElement("button"),o=new ConnectedDomain(this.host,e),i=document.createElement("span");i.innerText=Application.subDomainColonForUnderscore(this.host),i.setAttribute("class","app-host-span"),this.nameTD.appendChild(i),setElementText(n,"Disconnect"),n.setAttribute("class",CONTRAST_BUTTON_DISCONNECT_CLASS),n.addEventListener("click",()=>{o.disconnectDomain(this).then(t=>{if(!t)throw new Error("Error Disconnecting Domain");this.removeDomainAndButton()}).catch(t=>this._handleConnectError(t))}),this.buttonTD.appendChild(n)},TableRow.prototype.removeDomainAndButton=function(){this.buttonTD.innerHTML="",this.nameTD.innerHTML=encodeURIComponent(this.application.name)},TableRow.prototype._showMessage=function(t,e){const n=document.getElementById("connected-domain-message"),o=document.getElementById("table-container");changeElementVisibility(n),t&&e?(this._successConnect(n),n.setAttribute("style",`color: ${CONTRAST_GREEN}`),setElementDisplay(o,"none")):!t&&e?(this._failConnect(n),n.setAttribute("style",`color: ${CONTRAST_GREEN}`),setElementDisplay(o,"none")):t||e?(changeElementVisibility(n),setElementDisplay(o,"none")):(this._failDisconnect(n),n.setAttribute("style",`color: ${CONTRAST_RED}`),setElementDisplay(o,"none"))},TableRow.prototype._handleConnectError=function(t){const e=document.getElementById("error-message-footer");setElementDisplay(e,"block"),setElementText(e,`${t.toString()}`),setTimeout(()=>setElementDisplay(e,"none"),1e4)},TableRow.prototype._successConnect=function(t){setElementText(t,CONNECT_SUCCESS_MESSAGE),t.setAttribute("style",`color: ${CONTRAST_GREEN}`),chrome.runtime.sendMessage({action:APPLICATION_CONNECTED,data:{domains:this._addHTTProtocol(this.host)}})},TableRow.prototype._failConnect=function(t){setElementText(t,CONNECT_FAILURE_MESSAGE),t.setAttribute("style",`color: ${CONTRAST_RED}`)},TableRow.prototype._failDisconnect=function(t){setElementText(t,"Error Disconnecting"),t.setAttribute("style",`color: ${CONTRAST_RED}`)},TableRow.prototype._addHTTProtocol=function(t){let e=t=Application.subDomainColonForUnderscore(t),n=t;return e.includes("http://")||(e="http://"+t+"/*"),n.includes("https://")||(n="https://"+t+"/*"),[e,n]}; -------------------------------------------------------------------------------- /dist/js/background.js: -------------------------------------------------------------------------------- 1 | import Queue from"./queue.js";let QUEUE;import{TEAMSERVER_INDEX_PATH_SUFFIX,TEAMSERVER_ACCOUNT_PATH_SUFFIX,VALID_TEAMSERVER_HOSTNAMES,TEAMSERVER_PROFILE_PATH_SUFFIX,CONTRAST_RED,TRACES_REQUEST,GATHER_FORMS_ACTION,LOADING_DONE,APPLICATION_CONNECTED,APPLICATION_DISCONNECTED,getStoredCredentials,isCredentialed,isBlacklisted,updateTabBadge,removeLoadingBadge,loadingBadge,updateExtensionIcon}from"./util.js";import Application from"./models/Application.js";import Vulnerability from"./models/Vulnerability.js";import VulnerableTab from"./models/VulnerableTab.js";import DomainStorage from"./models/DomainStorage.js";let TAB_CLOSED=!1;function resetXHRRequests(){window.XHR_REQUESTS=[]}window.XHR_REQUESTS=[],window.PAGE_FINISHED_LOADING=!1;const XHRDomains=new DomainStorage;function _handleWebRequest(e){const{method:t,url:n}=e,a=n.split("?")[0],o=["OPTIONS"!==t,!isBlacklisted(n),!window.XHR_REQUESTS.includes(a)];window.PAGE_FINISHED_LOADING&&QUEUE.executionCount>0&&o.every(Boolean)&&(window.XHR_REQUESTS.push(a),Vulnerability.evaluateSingleURL(a,QUEUE.tab,QUEUE.application)),o.every(Boolean)&&window.XHR_REQUESTS.push(a)}async function _handleRuntimeOnMessage(e,t,n){if(e)switch(e.action){case TRACES_REQUEST:{const a=VulnerableTab.buildTabPath(n.url),o=new VulnerableTab(a,e.application.name);t({traces:(await o.getStoredTab())[o.vulnTabId]}),removeLoadingBadge(n);break}case APPLICATION_CONNECTED:XHRDomains.addDomainsToStorage(e.data.domains);break;case APPLICATION_DISCONNECTED:XHRDomains.removeDomainsFromStorage(e.data.domains);break;case LOADING_DONE:window.PAGE_FINISHED_LOADING=!0;break;default:return e}return e}async function _queueActions(e,t){QUEUE.setTab(e);const n=[getStoredCredentials(),Application.retrieveApplicationFromStorage(e)],a=await Promise.all(n);a||(updateTabBadge(e,"!",CONTRAST_RED),updateExtensionIcon(e,1)),a[0]?a[1]||updateExtensionIcon(e,1):updateExtensionIcon(e,2),QUEUE.setCredentialed(isCredentialed(a[0])),QUEUE.setApplication(a[1]);let o=[];t&&(o=await _gatherFormsFromPage(e)),QUEUE.addForms(o,!0),QUEUE.addXHRequests(window.XHR_REQUESTS,!0),QUEUE.executeQueue(resetXHRRequests)}function _tabIsReady(e,t){return(!e.favIconUrl||1!==Object.keys(e).length)&&(!(!t.active||!e.status)&&(!chrome.runtime.lastError&&(!(!t.url.includes("http://")&&!t.url.includes("https://"))&&("loading"!==e.status||(resetXHRRequests(),TAB_CLOSED||(loadingBadge(t),TAB_CLOSED=!1),!1)))))}function _gatherFormsFromPage(e){return new Promise(t=>isBlacklisted(e.url)?t([]):chrome.tabs.sendMessage(e.id,{action:GATHER_FORMS_ACTION},e=>e&&e.formActions&&Array.isArray(e.formActions)?t(e.formActions):t([])))}function notifyUserToConfigure(e){if(chrome.runtime.lastError)return;const t=new URL(e.url),n=[VALID_TEAMSERVER_HOSTNAMES.includes(t.hostname)&&e.url.endsWith(TEAMSERVER_ACCOUNT_PATH_SUFFIX),e.url.endsWith(TEAMSERVER_PROFILE_PATH_SUFFIX)&&-1!==e.url.indexOf(TEAMSERVER_INDEX_PATH_SUFFIX),!chrome.runtime.lastError];!TAB_CLOSED&&n.some(e=>!!e)&&(updateExtensionIcon(e,2),TAB_CLOSED=!1)}chrome.webRequest.onBeforeRequest.addListener(e=>{_handleWebRequest(e)},{urls:XHRDomains.domains,types:["xmlhttprequest"]}),chrome.runtime.onMessage.addListener((e,t,n)=>{const{tab:a}=e;return a&&a.active?(e.action!==TRACES_REQUEST&&e.action!==LOADING_DONE&&(TAB_CLOSED||(loadingBadge(a),TAB_CLOSED=!1)),a&&!isBlacklisted(a.url)?_handleRuntimeOnMessage(e,n,a):e.action===APPLICATION_DISCONNECTED?_handleRuntimeOnMessage(e,n,a):(removeLoadingBadge(a),n(null)),!0):(n("Tab not active"),!1)}),chrome.tabs.onActivated.addListener(e=>{window.PAGE_FINISHED_LOADING=!0,QUEUE=new Queue,chrome.tabs.get(e.tabId,e=>{e&&!chrome.runtime.lastError&&_queueActions(e,!1)})}),chrome.tabs.onUpdated.addListener((e,t,n)=>{QUEUE&&QUEUE.resetExecutionCount(),_tabIsReady(t,n)&&(QUEUE=new Queue,_queueActions(n,!0))}),chrome.tabs.onRemoved.addListener(()=>{TAB_CLOSED=!0});export{TAB_CLOSED,_handleRuntimeOnMessage,notifyUserToConfigure,resetXHRRequests}; -------------------------------------------------------------------------------- /test/spec/models/Application-spec.js: -------------------------------------------------------------------------------- 1 | const chrome = require('sinon-chrome/extensions'); 2 | global.chrome = chrome; 3 | const URL = require('url'); 4 | const sinon = require("sinon"); 5 | const chai = require("chai"); 6 | const { expect } = chai; 7 | const util = require('../../../lib/util.js'); 8 | const ApplicationModel = require('../../../lib/models/Application.js'); 9 | const Application = ApplicationModel.default; 10 | 11 | const APP_ID = "webgoat-id-123"; 12 | const APP_NAME = "webgoat"; 13 | const APP_HOST = "localhost_8080"; 14 | const APP = { 15 | [APP_HOST]: APP_ID, 16 | id: APP_ID, 17 | name: APP_NAME, 18 | domain: APP_HOST, 19 | host: APP_HOST, 20 | } 21 | 22 | const APP_ID_2 = "bhima-id-123"; 23 | const APP_NAME_2 = "bhima"; 24 | const APP_HOST_2 = "localhost:3000"; 25 | const APP_2 = { 26 | [APP_HOST_2]: APP_ID_2, 27 | id: APP_ID_2, 28 | name: APP_NAME_2, 29 | domain: APP_HOST_2, 30 | host: APP_HOST_2, 31 | } 32 | 33 | let { 34 | STORED_APPS_KEY, 35 | updateTabBadge, 36 | } = util; 37 | 38 | describe('tests for Application model', function() { 39 | beforeEach(function() { 40 | global.URL = URL.URL; 41 | chrome.flush(); 42 | chrome.reset(); 43 | chrome.runtime.sendMessage.flush(); 44 | chrome.runtime.sendMessage.reset(); 45 | chrome.storage.local.get.flush(); 46 | chrome.storage.local.get.reset(); 47 | chrome.tabs.query.flush(); 48 | chrome.tabs.query.reset(); 49 | }); 50 | 51 | after(function() { 52 | chrome.flush(); 53 | chrome.reset(); 54 | chrome.storage.local.get.flush(); 55 | chrome.storage.local.get.reset(); 56 | chrome.tabs.query.flush(); 57 | chrome.tabs.query.reset(); 58 | }); 59 | 60 | it('creates a new application', function() { 61 | const application = new Application(APP_HOST, { name: APP_NAME, app_id: APP_ID }); 62 | expect(JSON.stringify(application)).equal(JSON.stringify(APP)); 63 | }); 64 | 65 | it('resolves to null when no applications are in storage', function(done) { 66 | const tab = { url: "http://localhost:8080/WebGoat", id: 123 } 67 | const badgeSpy = sinon.spy(updateTabBadge); 68 | 69 | chrome.storage.local.get.yields({ [STORED_APPS_KEY]: [] }) 70 | Application.retrieveApplicationFromStorage(tab) 71 | .then(application => { 72 | expect(JSON.stringify(application)).equal(JSON.stringify(null)); 73 | expect(chrome.storage.local.get.called).equal(true); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('retrieves an app from storage', function(done) { 79 | const tab = { url: "http://localhost:8080/WebGoat", id: 123 }; 80 | 81 | chrome.storage.local.get.yields({ [STORED_APPS_KEY]: [APP] }) 82 | Application.retrieveApplicationFromStorage(tab) 83 | .then(application => { 84 | expect(JSON.stringify(application)).equal(JSON.stringify(APP)); 85 | expect(chrome.storage.local.get.called).equal(true); 86 | done(); 87 | }) 88 | .catch(done) 89 | }); 90 | 91 | it('filters an array of apps to get a specific app', function() { 92 | const app = { name: APP_NAME, app_id: APP_ID }; 93 | const storedApps = { [STORED_APPS_KEY]: [APP, APP_2]}; 94 | const storedApp = Application.getStoredApp(storedApps, app); 95 | expect(JSON.stringify(storedApp)).equal(JSON.stringify(APP)); 96 | }); 97 | 98 | it('converts apps with hosts with : to hosts with _', function() { 99 | const domain = Application.subDomainColonForUnderscore(APP_2); 100 | expect(domain).equal(APP_2.domain.replace(":", "_")); 101 | }); 102 | 103 | it('converts apps with hosts with _ to hosts with :', function() { 104 | const domain = Application.subDomainColonForUnderscore(APP); 105 | expect(domain).equal(APP.domain.replace("_", ":")); 106 | }); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /js/libraries/Library.js: -------------------------------------------------------------------------------- 1 | class Library { 2 | constructor(tab, library) { 3 | this.GET_LIB_VERSION = "GET_LIB_VERSION"; 4 | this.tab = tab; 5 | this.library = library; 6 | this.extractor = null; 7 | } 8 | 9 | _setExtrator(extractor) { 10 | this.extractor = extractor; 11 | } 12 | 13 | _setLibraryVersion(version) { 14 | this.library.version = version; 15 | } 16 | 17 | createVersionedLibrary() { 18 | if (this.library.extractors && this.library.extractors.func) { 19 | this._setExtrator(this.library.extractors.func[0]); 20 | return this._extractLibraryVersion(); // is a promise 21 | } 22 | return new Promise(resolve => resolve(this.library)); 23 | } 24 | 25 | _extractLibraryVersion() { 26 | return new Promise((resolve, reject) => { 27 | const { library, tab } = this; 28 | this._executeExtractionScript() 29 | // eslint-disable-next-line no-unused-vars 30 | .then(executed => { 31 | chrome.tabs.sendMessage( 32 | tab.id, 33 | { 34 | action: this.GET_LIB_VERSION, 35 | library 36 | }, 37 | version => { 38 | if (version) { 39 | this._setLibraryVersion(version); 40 | resolve(library); 41 | } else { 42 | this._setLibraryVersion( 43 | this._getVersionFromFileName(library.jsFileName) 44 | ); 45 | resolve(library); 46 | } 47 | } 48 | ); 49 | }) 50 | .catch(error => { 51 | reject(error); 52 | }); 53 | }); 54 | } 55 | 56 | _getVersionFromFileName(jsFileName) { 57 | const version = jsFileName.match(/\b\d+(?:\.\d+)*\b/); 58 | if (version) { 59 | return version[0]; 60 | } 61 | return null; 62 | } 63 | 64 | _executeExtractionScript() { 65 | return new Promise(resolve => { 66 | const { extractor, library, tab } = this; 67 | const details = { 68 | code: this._generateScriptTags({ extractor, library }) 69 | }; 70 | chrome.tabs.executeScript(tab.id, details, result => { 71 | resolve(!!result); 72 | }); 73 | }); 74 | } 75 | 76 | /** 77 | * NOTE: THIS IS NUTS 78 | * Necessary for executing a script on the webpage directly since 79 | * content scripts run in an isolated world 80 | * chrome.tabs.executeScript injects into content-script, not the page 81 | * 82 | * _generateScriptTags - Get the library version by running an extractor 83 | * function provided by Retire.js on the webpage, create an element which holds that value 84 | * 85 | * @param {Object} request request from content script 86 | * @return {String} script executed on webpage 87 | */ 88 | _generateScriptTags() { 89 | const { extractor } = this; 90 | if (!this.library.parsedLibName || !extractor) { 91 | return null; 92 | } 93 | const library = this.library.parsedLibName.replace("-", "_"); 94 | const script = ` 95 | try { 96 | var _c_res${library} = ${extractor}; 97 | var __docRes${library} = document.getElementById('__script_res_${library}'); 98 | __docRes${library}.innerText = _c_res${library}; 99 | } catch (e) { 100 | // console.log(e) // error getting libraries 101 | }`; 102 | 103 | return `try { 104 | var script${library} = document.createElement('script'); 105 | var scriptRes${library} = document.createElement('span'); 106 | script${library}.innerHTML = \`${script}\`; 107 | const elId_${library} = '__script_res_${library}' 108 | const el_${library} = document.getElementById(elId_${library}); 109 | if (!el_${library}) { 110 | scriptRes${library}.setAttribute('id', elId_${library}); 111 | document.body.appendChild(scriptRes${library}); 112 | document.body.appendChild(script${library}); 113 | scriptRes${library}.style.display = 'none'; 114 | } 115 | } catch (e) {}`; 116 | } 117 | } 118 | 119 | export default Library; 120 | -------------------------------------------------------------------------------- /dist/js/content-scripts/content-script.js: -------------------------------------------------------------------------------- 1 | "use strict";function _dataQuery(e){document.querySelector('[key-status-rotated=""]');const t=document.querySelector(`[data-contrast-scrape=${{0:"contrast-api-key",1:"contrast-organization-uuid",2:"contrast-contrast-url",3:"contrast-service-key",4:"contrast-username"}[e]}]`);if(t)return t.textContent}function _scrapeServiceKey(){const e=document.querySelector('[key-status-rotated="userKeyStatus.rotated"]');return e?e.innerText:null}function _scrapeApiKey(){const e=document.querySelector("span.break-word.org-key.ng-binding");return e?e.innerText:null}function _scrapeOrgUUID(){const e=document.location.hash.split("/")[1];return UUID_V4_REGEX.test(e)?e:null}function _scrapeProfileEmail(){const e=document.querySelector(".profile-email");return e?e.innerText:null}function _initializeContrast(e,t){const r=e.url.indexOf(TEAMSERVER_INDEX_PATH_SUFFIX),n=e.url.substring(0,r)+TEAMSERVER_API_PATH_SUFFIX||_dataQuery(2),i=_scrapeApiKey()||_dataQuery(0),a=_scrapeServiceKey()||_dataQuery(3),o=_scrapeOrgUUID()||_dataQuery(1),s=_scrapeProfileEmail()||_dataQuery(4),c={[CONTRAST_USERNAME]:s,[CONTRAST_SERVICE_KEY]:a,[CONTRAST_API_KEY]:i,[CONTRAST_ORG_UUID]:o,[TEAMSERVER_URL]:n};Object.values(c).forEach(e=>{e||t({action:CONTRAST_INITIALIZED,success:!1,message:"Failed to configure extension. Try configuring the extension manually."})}),chrome.storage.local.set(c,()=>{if(chrome.runtime.lastError)throw new Error("Error setting configuration");t({action:CONTRAST_INITIALIZED,success:!0,contrastObj:c})})}function _getLibraryVulnerabilities(){return fetch("https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json",{method:"GET"}).then(e=>e.ok&&200===e.status?e.json():null).catch(new Error("Error getting js lib vulnerabilities"))}async function _collectScripts(e){const t=await _getLibraryVulnerabilities();if(!t)return null;const r=await wappalzye(e),n=_compareAppAndVulnerableLibraries([].slice.call(document.scripts).map(e=>{let t=e.src.split("/");return t[t.length-1]}),r,t);return n&&0!==n.length?{sharedLibraries:n}:null}function _compareAppAndVulnerableLibraries(e,t,r){return t=t.map(e=>{let t=e.name.toLowerCase();return e.jsFileName=t,e.parsedLibName=t,e.parsedLibNameJS=t+".js",e}),_findCommonLibraries(r,e.map(e=>{if(e&&e[0]&&new RegExp(/[a-z]/).test(e[0])){let t=e,r=_getLibNameFromJSFile(e);return{jsFileName:t,parsedLibName:r,parsedLibNameJS:r+".js"}}return!1}).filter(Boolean),t)}function _findCommonLibraries(e,t,r){let n=[];for(let i in e)if(Object.prototype.hasOwnProperty.call(e,i)){let a=e[i],o=[];if(o.push(i),a.bowername){let e=a.bowername.map(e=>e.toLowerCase());o=o.concat(e)}let s=r.filter(e=>(e.name=e.name.toLowerCase(),o.includes(e.parsedLibName)||o.includes(e.parsedLibNameJS))),c=t.filter(e=>o.includes(e.jsFileName)||o.includes(e.parsedLibName)||o.includes(e.parsedLibNameJS)),l=s.concat(c);if(l[0]){if(!n.find(e=>l[0].parsedLibName===e.parsedLibName)){const e=l[0],t=a.extractors;e.name=i,e.extractors=t,e.vulnerabilities=a.vulnerabilities,n.push(e)}}}return n}function _getLibNameFromJSFile(e){return e=(e=(e=(e=(e=e.split(".js")[0]).split(".min")[0]).split("-min")[0]).split("_min")[0]).match(/([a-zA-Z]+\W)+/)?e.match(/([a-zA-Z]+\W)+/)[0]:e,e=new RegExp(/\W/).test(e[e.length-1])?e.substr(0,e.length-1):e}function wappalzye(e){return new Promise(t=>{chrome.runtime.sendMessage({action:CONTRAST_WAPPALIZE,tab:e},e=>{t(e)})})}1===window.performance.navigation.type?window.CONTRAST__REFRESHED=!0:window.CONTRAST__REFRESHED=!1,window.addEventListener("load",function(){chrome.runtime.sendMessage({action:LOADING_DONE}),setTimeout(function(){window.CONTRAST__REFRESHED=!1},1e3)}),chrome.runtime.onMessage.addListener((e,t,r)=>{if(e.action===GATHER_FORMS_ACTION)document.getElementsByTagName("form").length>0?setTimeout(()=>ContrastForm.collectFormActions(r),1e3):ContrastForm.collectFormActions(r);else if(e.action===HIGHLIGHT_VULNERABLE_FORMS)r(ContrastForm.highlightForms(e.formActions));else if(void 0!==e.url&&e.action===CONTRAST_INITIALIZE)_initializeContrast(e,r);else if("GET_LIB_VERSION"===e.action&&e.library){const t=e.library.parsedLibName.replace("-","_"),n=document.getElementById(`__script_res_${t}`);let i;try{i=n.innerText}catch(e){r(null)}if(i){let e=i.split("_");r(e[e.length-1])}else r(null)}else e.action===GATHER_SCRIPTS&&_collectScripts(e.tab).then(e=>r(e)).catch(e=>{});return!0}); -------------------------------------------------------------------------------- /lib/models/VulnerableTab.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _regenerator = require("babel-runtime/regenerator"); 8 | 9 | var _regenerator2 = _interopRequireDefault(_regenerator); 10 | 11 | var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator"); 12 | 13 | var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); 14 | 15 | var _promise = require("babel-runtime/core-js/promise"); 16 | 17 | var _promise2 = _interopRequireDefault(_promise); 18 | 19 | var _util = require("../util.js"); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function VulnerableTabError(message, vulnTabId, vulnTabUrl) { 24 | throw new Error(message, vulnTabId, vulnTabUrl); 25 | } /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 26 | 27 | 28 | function VulnerableTab(path, applicationName) { 29 | var traces = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; 30 | 31 | this.traceIDs = traces; 32 | this.path = path.split("?")[0]; 33 | this.vulnTabId = (0, _util.murmur)(this.path + "|" + applicationName); 34 | this.appNameHash = (0, _util.murmur)(applicationName); 35 | } 36 | 37 | VulnerableTab.prototype.setTraceIDs = function (traceIDs) { 38 | this.traceIDs = (0, _util.deDupeArray)(this.traceIDs.concat(traceIDs)); 39 | }; 40 | 41 | VulnerableTab.prototype.storeTab = function () { 42 | var _this = this; 43 | 44 | return new _promise2.default(function () { 45 | var _ref = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee(resolve, reject) { 46 | var appTabs; 47 | return _regenerator2.default.wrap(function _callee$(_context) { 48 | while (1) { 49 | switch (_context.prev = _context.next) { 50 | case 0: 51 | _context.next = 2; 52 | return _this.getApplicationTabs(); 53 | 54 | case 2: 55 | appTabs = _context.sent; 56 | 57 | appTabs[_this.appNameHash][_this.vulnTabId] = _this.traceIDs; 58 | 59 | chrome.storage.local.set(appTabs, function () { 60 | chrome.storage.local.get(_this.appNameHash, function (storedTab) { 61 | if (storedTab && storedTab[_this.appNameHash]) { 62 | resolve(storedTab[_this.appNameHash]); 63 | } else { 64 | reject(new VulnerableTabError("Error Storing Tab", _this.vulnTabId, _this.path)); 65 | } 66 | }); 67 | }); 68 | 69 | case 5: 70 | case "end": 71 | return _context.stop(); 72 | } 73 | } 74 | }, _callee, _this); 75 | })); 76 | 77 | return function (_x2, _x3) { 78 | return _ref.apply(this, arguments); 79 | }; 80 | }()); 81 | }; 82 | 83 | VulnerableTab.prototype.getApplicationTabs = function () { 84 | var _this2 = this; 85 | 86 | return new _promise2.default(function (resolve) { 87 | chrome.storage.local.get(_this2.appNameHash, function (appTabs) { 88 | 89 | // NOTE: if an application has just been added, appTabs will be empty obj 90 | // Add appNameHash key with val as empty object for storing vulnTabIds 91 | if (!appTabs || (0, _util.isEmptyObject)(appTabs)) { 92 | appTabs[_this2.appNameHash] = {}; 93 | } 94 | 95 | resolve(appTabs); 96 | }); 97 | }); 98 | }; 99 | 100 | VulnerableTab.prototype.getStoredTab = function () { 101 | var _this3 = this; 102 | 103 | return new _promise2.default(function (resolve) { 104 | chrome.storage.local.get(_this3.appNameHash, function (storedTabs) { 105 | if (storedTabs && storedTabs[_this3.appNameHash]) { 106 | resolve(storedTabs[_this3.appNameHash]); 107 | } else { 108 | resolve(null); 109 | } 110 | }); 111 | }); 112 | }; 113 | 114 | VulnerableTab.buildTabPath = function (tabUrl) { 115 | var url = new URL(tabUrl); 116 | var path = url.pathname; 117 | if (url.hash) { 118 | path += url.hash; 119 | } 120 | return path; 121 | }; 122 | 123 | exports.default = VulnerableTab; -------------------------------------------------------------------------------- /js/models/ConnectedDomain.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | import { 3 | STORED_APPS_KEY, 4 | setElementDisplay, 5 | } from '../util.js' 6 | 7 | import Application from './Application.js' 8 | 9 | export default function ConnectedDomain(host, application) { 10 | this.host = host; 11 | this.application = application; 12 | } 13 | 14 | ConnectedDomain.prototype.connectDomain = function() { 15 | return this._addDomainToStorage(); 16 | } 17 | 18 | /** 19 | * _addDomainToStorage - add a domain + app name connection to chrome storage 20 | * 21 | * @param {String} host the host/domain of the application 22 | * @param {String} application the name of the application 23 | * @return {Promise} if storing the data succeeded 24 | */ 25 | ConnectedDomain.prototype._addDomainToStorage = function() { 26 | const { host, application } = this; 27 | 28 | return new Promise((resolve, reject) => { 29 | chrome.storage.local.get(STORED_APPS_KEY, (result) => { 30 | if (chrome.storage.lastError) { 31 | return reject(new Error("Error retrieving stored apps")); 32 | } 33 | 34 | // no applications stored so result[STORED_APPS_KEY] is undefined 35 | if (!result[STORED_APPS_KEY]) result[STORED_APPS_KEY] = []; 36 | 37 | // Verify that the domain of the app to be connected isn't already in use by the extension 38 | const verified = this._verifyDomainNotInUse(result[STORED_APPS_KEY], host); 39 | if (verified.constructor === Error) { 40 | return reject(verified); 41 | } 42 | 43 | const app = new Application(host, application); 44 | 45 | const updatedStoredApps = result[STORED_APPS_KEY].concat(app); 46 | 47 | const applicationTable = document.getElementById("application-table"); 48 | chrome.storage.local.set({ [STORED_APPS_KEY]: updatedStoredApps }, () => { 49 | setElementDisplay(applicationTable, "none"); 50 | resolve(!chrome.storage.lastError); 51 | }); 52 | }); 53 | }); 54 | } 55 | 56 | ConnectedDomain.prototype._verifyDomainNotInUse = function(storedApps, host) { 57 | if (storedApps.length > 0) { 58 | for (let i = 0, len = storedApps.length; i < len; i++) { 59 | let app = storedApps[i]; 60 | if (app.domain === host) { 61 | return new Error(`The Domain ${Application._subColonOrUnderscore(host)} is already in use by another application: ${app.name}. Please either first disconnect ${app.name} or run this application on a different domain/port.`); 62 | } 63 | } 64 | } 65 | return true; 66 | } 67 | 68 | 69 | ConnectedDomain.prototype.disconnectDomain = function() { 70 | return this._removeDomainFromStorage() 71 | } 72 | 73 | /** 74 | * _removeDomainFromStorage - removes an application + domain connection from storage 75 | * 76 | * @param {String} host the host/domain of the application 77 | * @param {Array} storedApps the array of stored apps 78 | * @param {String} application the name of the application to remove 79 | * @param {Node} disconnectButton button user clicks remove an application 80 | * @return {Promise} if the removal succeeded 81 | */ 82 | ConnectedDomain.prototype._removeDomainFromStorage = function() { 83 | return new Promise((resolve, reject) => { 84 | 85 | chrome.storage.local.get(STORED_APPS_KEY, (result) => { 86 | const updatedStoredApps = this._filterOutApp(result); 87 | 88 | chrome.storage.local.set({ [STORED_APPS_KEY]: updatedStoredApps }, () => { 89 | if (chrome.runtime.lastError) { 90 | reject(new Error(chrome.runtime.lastError)); 91 | } 92 | resolve(!chrome.runtime.lastError); 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | /** 99 | * @description ConnectedDomain.prototype._filterOutApp - create a new array of connected apps that does not include the application belonging to this 100 | * 101 | * @param {Array} storedApps - connected apps in chrome storage 102 | * @return {Array} - filtered apps in chrome storage 103 | */ 104 | ConnectedDomain.prototype._filterOutApp = function(storedApps) { 105 | return storedApps[STORED_APPS_KEY].filter(app => { 106 | return app.id !== this.application.id; 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /js/models/Application.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | STORED_APPS_KEY, 4 | CONTRAST_GREEN, 5 | getHostFromUrl, 6 | isBlacklisted, 7 | updateTabBadge, 8 | updateExtensionIcon, 9 | isEmptyObject 10 | } from "../util.js"; 11 | 12 | export default function Application(host, teamserverApplication) { 13 | this[host] = teamserverApplication.app_id; 14 | this.id = teamserverApplication.app_id; 15 | this.name = teamserverApplication.name; 16 | this.domain = host; 17 | this.host = host; 18 | } 19 | 20 | /** 21 | * retrieveApplicationFromStorage - get the name of an application from storage by using those host/domain name of the current tab url 22 | * 23 | * @param {Object} tab - the active tab in the active window 24 | * @return {Promise} - a connected application 25 | */ 26 | Application.retrieveApplicationFromStorage = function(tab) { 27 | return new Promise((resolve, reject) => { 28 | chrome.storage.local.get(STORED_APPS_KEY, result => { 29 | if (chrome.runtime.lastError) { 30 | reject(new Error("Error retrieving stored applications")); 31 | } 32 | 33 | if (!result || !result[STORED_APPS_KEY]) { 34 | result = { [STORED_APPS_KEY]: [] }; 35 | } 36 | 37 | const url = new URL(tab.url); 38 | const host = getHostFromUrl(url); 39 | 40 | const application = result[STORED_APPS_KEY].filter(app => { 41 | return app.host === host; 42 | })[0]; 43 | // application = result[STORED_APPS_KEY].filter(app => app[host])[0]; 44 | 45 | if (!application) { 46 | if (!isBlacklisted(tab.url) && !chrome.runtime.lastError) { 47 | try { 48 | updateExtensionIcon(tab, 1); 49 | updateTabBadge(tab, ""); 50 | // updateTabBadge(tab, CONTRAST_CONFIGURE_TEXT, CONTRAST_YELLOW); 51 | } catch (e) { 52 | reject(new Error("Error updating tab badge")); 53 | } 54 | } else if (isBlacklisted(tab.url) && !chrome.runtime.lastError) { 55 | try { 56 | updateExtensionIcon(tab, 1); 57 | updateTabBadge(tab, "", CONTRAST_GREEN); 58 | } catch (e) { 59 | reject(new Error("Error updating tab badge")); 60 | } 61 | } 62 | resolve(null); 63 | } else { 64 | if (application && application.name) { 65 | const appLibText = document.getElementById('scan-libs-text'); 66 | if (appLibText) { 67 | appLibText.innerText = `Current Application:\n${application.name.trim()}`; 68 | } 69 | } 70 | resolve(application); 71 | } 72 | }); 73 | }); 74 | }; 75 | 76 | /** 77 | * @description - filters an array of storedapps to get a single application 78 | * 79 | * @param {Array} storedApps - connected apps in chrome storage 80 | * @param {Object} application - an application from an Org 81 | * @return {Array} - array of 1 connected app 82 | */ 83 | Application.getStoredApp = function(appsInStorage, application) { 84 | if (!application) throw new Error("application must be defined"); 85 | 86 | if (isEmptyObject(appsInStorage)) return; 87 | const storedApps = appsInStorage[STORED_APPS_KEY] || appsInStorage; 88 | return storedApps.filter(app => { 89 | return app.id === application.app_id; 90 | })[0]; 91 | }; 92 | 93 | /** 94 | * @description - can't use a colon in app name (because objects), sub colon for an underscore. Needed when dealing with ports in app name like localhost:8080 95 | * 96 | * @param {Application/String} storedApp - the connected application 97 | * @return {String} - app domain with colon swapped in/out 98 | */ 99 | Application.subDomainColonForUnderscore = function(storedApp) { 100 | if (typeof storedApp === "object") { 101 | return this._subColonOrUnderscore(storedApp.domain); 102 | } 103 | // storedApp is a string 104 | return this._subColonOrUnderscore(storedApp); 105 | }; 106 | 107 | /** 108 | * @description - Replaces all colons with underscores or all underscores with colons 109 | * 110 | * @param {String} string - the string to replace the characters on 111 | * @return {String} - a string with characters replaced 112 | */ 113 | Application._subColonOrUnderscore = function(string) { 114 | if (string.includes("_")) { 115 | return string.replace("_", ":"); // local dev stuff 116 | } else if (string.includes(":")) { 117 | return string.replace(":", "_"); // local dev stuff 118 | } 119 | return string; 120 | }; 121 | -------------------------------------------------------------------------------- /test/spec/models/Config-spec.js: -------------------------------------------------------------------------------- 1 | const chrome = require("sinon-chrome/extensions"); 2 | global.chrome = chrome; 3 | 4 | const url = require("url"); 5 | global.URL = url.URL; 6 | 7 | const jsdom = require("jsdom"); 8 | const { JSDOM } = jsdom; 9 | global.window = new JSDOM("", { url: "http://localhost" }).window; 10 | global.document = global.window.document; 11 | 12 | const sinon = require("sinon"); 13 | const chai = require("chai"); 14 | const { expect } = chai; 15 | const util = require('../../../lib/util.js'); 16 | const testData = require('../../testData.js'); 17 | const Config = require('../../../lib/models/Config.js').default; 18 | 19 | const { 20 | fakeTab, // url: "http://localhost:8080/WebGoat" 21 | fakeCreds, 22 | } = testData; 23 | 24 | /************************************************** 25 | * NOTE: Popup Screen tests are handled by Snapshots 26 | **************************************************/ 27 | describe('tests for Config model', function() { 28 | let config; 29 | beforeEach(function() { 30 | chrome.flush(); 31 | chrome.reset(); 32 | chrome.runtime.sendMessage.flush(); 33 | chrome.runtime.sendMessage.reset(); 34 | chrome.storage.local.get.flush(); 35 | chrome.storage.local.get.reset(); 36 | chrome.tabs.query.flush(); 37 | chrome.tabs.query.reset(); 38 | }); 39 | 40 | after(function() { 41 | config = undefined; 42 | chrome.flush(); 43 | chrome.reset(); 44 | chrome.storage.local.get.flush(); 45 | chrome.storage.local.get.reset(); 46 | chrome.tabs.query.flush(); 47 | chrome.tabs.query.reset(); 48 | }); 49 | 50 | it('creates a new Config', function() { 51 | let expectedConfig = { 52 | tab: fakeTab, 53 | url: fakeTab.url, 54 | credentialed: true, 55 | credentials: fakeCreds, 56 | hasApp: false, 57 | } 58 | config = new Config(fakeTab, new URL(fakeTab.url), true, fakeCreds, false); 59 | expect(config instanceof Config).equal(true); 60 | expect(JSON.stringify(expectedConfig)).equal(JSON.stringify(config)); 61 | }); 62 | 63 | it('builds a contrast url', function() { 64 | let input = "localhost:1234"; 65 | config = new Config(fakeTab, new URL(fakeTab.url), true, fakeCreds, true); 66 | expect(config._buildContrastUrl(input)).equal(`http://${input}/Contrast/api`); 67 | 68 | input = "app.contrastsecurity.com"; 69 | expect(config._buildContrastUrl(input)).equal(`https://${input}/Contrast/api`); 70 | 71 | // NOTE: Calling .throw(), pass a function not the result of a function 72 | // https://stackoverflow.com/a/21587239/6410635 73 | input = "javascript:alert(1)"; 74 | expect(config._buildContrastUrl.bind(config, input)).to.throw(); 75 | }); 76 | 77 | it('is a contrast page', function() { 78 | fakeTab.url = "http://app.contrastsecurity.com/Contrast/static/ng"; 79 | let url = "http://app.contrastsecurity.com/Contrast/static/ng"; 80 | config = new Config(fakeTab, new URL(url), true, fakeCreds, true); 81 | expect(config._isContrastPage()).equal(true); 82 | }); 83 | 84 | it('is the contrast your account page', function() { 85 | fakeTab.url = "http://app.contrastsecurity.com/Contrast/static/ng/index.html#/account"; 86 | let url = "http://app.contrastsecurity.com/Contrast/static/ng/index.html#/account"; 87 | config = new Config(fakeTab, new URL(url), true, fakeCreds, true); 88 | expect(config._isTeamserverAccountPage()).equal(true); 89 | }); 90 | 91 | it('gets a contrast url', function() { 92 | config = new Config(fakeTab, new URL(fakeTab.url), true, fakeCreds, true); 93 | let stub = sinon.stub(config, '_isContrastPage'); 94 | stub.returns(true); 95 | 96 | let origin = config.url.origin; 97 | 98 | let input = document.createElement('input'); 99 | document.body.appendChild(input); 100 | input.value = ""; 101 | 102 | expect(config._getContrastURL(input)).equal(`${origin}/Contrast/api`); 103 | }); 104 | 105 | it('gets a contrast url with an input value', function() { 106 | config = new Config(fakeTab, new URL(fakeTab.url), true, fakeCreds, true); 107 | let stub = sinon.stub(config, '_isContrastPage'); 108 | stub.returns(false); 109 | 110 | let origin = config.url.origin; 111 | 112 | let input = document.createElement('input'); 113 | document.body.appendChild(input); 114 | input.value = "hello"; 115 | 116 | expect(config._getContrastURL(input)).equal(`https://${input.value}/Contrast/api`); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /js/queue.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | import { isBlacklisted, removeLoadingBadge, deDupeArray } from "./util.js"; 3 | 4 | import Vulnerability from "./models/Vulnerability.js"; 5 | 6 | class Queue { 7 | constructor() { 8 | this.xhrRequests = []; 9 | this.gatheredForms = []; 10 | this.traceIDs = []; 11 | this.xhrReady = false; 12 | this.formsReady = false; 13 | this.isCredentialed = false; 14 | this.tab = null; 15 | this.application = null; 16 | this.tabUrl = ""; 17 | this.executionCount = 0; 18 | } 19 | 20 | addXHRequests(requests, xhrReady) { 21 | this.xhrReady = xhrReady; 22 | this.xhrRequests = this.xhrRequests.concat(requests); 23 | } 24 | 25 | addForms(forms, formsReady) { 26 | this.formsReady = formsReady; 27 | this.gatheredForms = this.gatheredForms.concat(forms); 28 | } 29 | 30 | setTab(tab) { 31 | if (!tab.url) throw new Error("Tab URL is falsey, received", tab.url); 32 | this.tab = tab; 33 | this.tabUrl = tab.url; 34 | } 35 | 36 | setApplication(application) { 37 | this.application = application; 38 | } 39 | 40 | setCredentialed(credentialed) { 41 | this.isCredentialed = credentialed; 42 | } 43 | 44 | _increaseExecutionCount() { 45 | this.executionCount += 1; 46 | } 47 | 48 | resetExecutionCount() { 49 | this.executionCount = 0; 50 | } 51 | 52 | /** 53 | * NOTE: Not using, doesn't reset *this*, only resets instance 54 | */ 55 | // resetQueue() { 56 | // this.xhrRequests = []; 57 | // this.gatheredForms = []; 58 | // this.traceIDs = []; 59 | // this.xhrReady = false; 60 | // this.formsReady = false; 61 | // this.isCredentialed = false; 62 | // this.tab = null; 63 | // this.application = null; 64 | // this.tabUrl = ""; 65 | // this.executionCount = 0; 66 | // } 67 | 68 | _highLightVulnerableForms(formTraces) { 69 | const highlightActions = formTraces 70 | .map(ft => { 71 | if (ft.traces && ft.traces.length > 0) { 72 | return ft.action; 73 | } 74 | return false; 75 | }) 76 | .filter(Boolean); 77 | Vulnerability.highlightForms(this.tab, highlightActions); 78 | } 79 | 80 | _evaluateForms() { 81 | return Vulnerability.evaluateFormActions( 82 | this.gatheredForms, 83 | this.tab, 84 | this.application 85 | ); 86 | } 87 | 88 | async executeQueue(resetXHRRequests) { 89 | // NOTE: At start loading badge still true 90 | 91 | // If tab URL is blacklisted, don't process anything 92 | const url = this.tabUrl || this.tab.url; 93 | if (isBlacklisted(url)) { 94 | removeLoadingBadge(this.tab); 95 | return; 96 | } 97 | 98 | const conditions = [ 99 | this.xhrReady, 100 | this.formsReady, 101 | this.tab, 102 | this.tabUrl, 103 | this.isCredentialed, 104 | this.application 105 | ]; 106 | 107 | if (!conditions.every(Boolean)) { 108 | // console.log("Queue not ready to execute!", conditions); 109 | return; 110 | } 111 | 112 | await Vulnerability.removeVulnerabilitiesFromStorage(this.tab); //eslint-disable-line 113 | 114 | // NOTE: In order to highlight vulnerable forms, form actions must be evaluated separately 115 | const formTraces = await this._evaluateForms(); 116 | 117 | if (formTraces && formTraces.length > 0) { 118 | this._highLightVulnerableForms(formTraces); 119 | } 120 | 121 | let traceUrls = this.xhrRequests.concat([this.tabUrl]); 122 | traceUrls = traceUrls.filter(tu => !isBlacklisted(tu)); 123 | traceUrls = traceUrls.map(trace => new URL(trace).pathname); 124 | 125 | Vulnerability.evaluateVulnerabilities( 126 | this.isCredentialed, // if credentialed already 127 | this.tab, // current tab 128 | deDupeArray(traceUrls), // gathered xhr requests from page load 129 | this.application, // current app 130 | formTraces ? formTraces.map(f => f.traces).flatten() : [], 131 | resetXHRRequests 132 | ); 133 | 134 | this._increaseExecutionCount(); 135 | // NOTE: At end, badge is number of vulnerabilities 136 | } 137 | } 138 | 139 | export default Queue; 140 | 141 | /** 142 | * 1. Check that application for tab URL has been connected 143 | * 2. Check that user has configured and has credentials 144 | * 3. Wait for page to load 145 | * 3a. Capture XHR Requests 146 | * 4. Scrape for forms 147 | * 5. Execute on stored XHR, forms and tab url 148 | * 6. Continuously evaluate XHR 149 | */ 150 | -------------------------------------------------------------------------------- /lib/libraries/VulnerableApplicationLibrary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 8 | 9 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 10 | 11 | var _createClass2 = require('babel-runtime/helpers/createClass'); 12 | 13 | var _createClass3 = _interopRequireDefault(_createClass2); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var VulnerableApplicationLibrary = function () { 18 | function VulnerableApplicationLibrary(vulnerableLibrary) { 19 | (0, _classCallCheck3.default)(this, VulnerableApplicationLibrary); 20 | 21 | this.name = vulnerableLibrary.name || vulnerableLibrary.parsedLibName; 22 | this.vulnerabilities = vulnerableLibrary.vulnerabilities; 23 | this.vulnerabilitiesCount = vulnerableLibrary.vulnerabilities.length; 24 | this.version = vulnerableLibrary.version; 25 | } 26 | 27 | (0, _createClass3.default)(VulnerableApplicationLibrary, [{ 28 | key: 'lowConfidenceVulnerabilities', 29 | value: function lowConfidenceVulnerabilities() { 30 | return { 31 | name: this.name, 32 | confidenceIsCorrectLibrary: 'LOW', 33 | vulnerabilitiesCount: this.vulnerabilitiesCount, 34 | vulnerabilities: this.vulnerabilities.map(function (v) { 35 | var versions = {}; 36 | v.atOrAbove ? versions.atOrAbove = v.atOrAbove : null; 37 | v.atOrBelow ? versions.atOrBelow = v.atOrBelow : null; 38 | v.above ? versions.above = v.above : null; 39 | v.below ? versions.below = v.below : null; 40 | return { 41 | title: v.identifiers.summary, 42 | link: v.info[0], 43 | severity: v.severity, 44 | versions: versions 45 | }; 46 | }) 47 | }; 48 | } 49 | }, { 50 | key: 'highConfidenceVulnerability', 51 | value: function highConfidenceVulnerability() { 52 | var vulnObj = this._isCorrectVersion(this.vulnerabilities, this.version); 53 | if (!vulnObj) return vulnObj; 54 | return { 55 | name: this.name, 56 | severity: vulnObj.severity, 57 | title: vulnObj.identifiers.summary, 58 | link: vulnObj.info[0], 59 | confidenceIsCorrectLibrary: 'HIGH', 60 | vulnerabilities: this.vulnerabilities, 61 | vulnerabilitiesCount: this.vulnerabilitiesCount 62 | }; 63 | } 64 | }, { 65 | key: '_isCorrectVersion', 66 | value: function _isCorrectVersion(vulnerabilityObjects, libVersion) { 67 | if (!vulnerabilityObjects || !libVersion) return false; 68 | 69 | for (var i = 0, len = vulnerabilityObjects.length; i < len; i++) { 70 | var vuln = vulnerabilityObjects[i]; 71 | var below = vuln.below, 72 | atOrAbove = vuln.atOrAbove, 73 | above = vuln.above; 74 | 75 | if (below) { 76 | below = this._parseVersionNumber(below); 77 | } 78 | if (atOrAbove) { 79 | atOrAbove = this._parseVersionNumber(atOrAbove); 80 | } 81 | if (above) { 82 | above = this._parseVersionNumber(above); 83 | } 84 | if (this._hasVulnerableVersion(below, atOrAbove, above, libVersion)) { 85 | return vuln; 86 | } 87 | 88 | // get script obj that has matching bowername 89 | // compare script vuln version to vulnObj versions 90 | // true if is correct version 91 | } 92 | return null; 93 | } 94 | }, { 95 | key: '_hasVulnerableVersion', 96 | value: function _hasVulnerableVersion(below, atOrAbove, above, libVersion) { 97 | if (below && atOrAbove) { 98 | if (libVersion < below && libVersion >= atOrAbove) { 99 | return true; 100 | } 101 | } else if (below && above) { 102 | if (libVersion < below && libVersion > above) { 103 | return true; 104 | } 105 | } else if (below && libVersion < below) { 106 | return true; 107 | } else if (atOrAbove && libVersion >= atOrAbove) { 108 | return true; 109 | } else if (above && libVersion > above) { 110 | return true; 111 | } 112 | return false; 113 | } 114 | }, { 115 | key: '_parseVersionNumber', 116 | value: function _parseVersionNumber(string) { 117 | return string.split("-")[0].split(/[a-zA-Z]/)[0]; 118 | } 119 | }]); 120 | return VulnerableApplicationLibrary; 121 | }(); 122 | 123 | exports.default = VulnerableApplicationLibrary; -------------------------------------------------------------------------------- /lib/models/ConnectedDomain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _defineProperty2 = require('babel-runtime/helpers/defineProperty'); 8 | 9 | var _defineProperty3 = _interopRequireDefault(_defineProperty2); 10 | 11 | var _promise = require('babel-runtime/core-js/promise'); 12 | 13 | var _promise2 = _interopRequireDefault(_promise); 14 | 15 | exports.default = ConnectedDomain; 16 | 17 | var _util = require('../util.js'); 18 | 19 | var _Application = require('./Application.js'); 20 | 21 | var _Application2 = _interopRequireDefault(_Application); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 26 | function ConnectedDomain(host, application) { 27 | this.host = host; 28 | this.application = application; 29 | } 30 | 31 | ConnectedDomain.prototype.connectDomain = function () { 32 | return this._addDomainToStorage(); 33 | }; 34 | 35 | /** 36 | * _addDomainToStorage - add a domain + app name connection to chrome storage 37 | * 38 | * @param {String} host the host/domain of the application 39 | * @param {String} application the name of the application 40 | * @return {Promise} if storing the data succeeded 41 | */ 42 | ConnectedDomain.prototype._addDomainToStorage = function () { 43 | var _this = this; 44 | 45 | var host = this.host, 46 | application = this.application; 47 | 48 | 49 | return new _promise2.default(function (resolve, reject) { 50 | chrome.storage.local.get(_util.STORED_APPS_KEY, function (result) { 51 | if (chrome.storage.lastError) { 52 | return reject(new Error("Error retrieving stored apps")); 53 | } 54 | 55 | // no applications stored so result[STORED_APPS_KEY] is undefined 56 | if (!result[_util.STORED_APPS_KEY]) result[_util.STORED_APPS_KEY] = []; 57 | 58 | // Verify that the domain of the app to be connected isn't already in use by the extension 59 | var verified = _this._verifyDomainNotInUse(result[_util.STORED_APPS_KEY], host); 60 | if (verified.constructor === Error) { 61 | return reject(verified); 62 | } 63 | 64 | var app = new _Application2.default(host, application); 65 | 66 | var updatedStoredApps = result[_util.STORED_APPS_KEY].concat(app); 67 | 68 | var applicationTable = document.getElementById("application-table"); 69 | chrome.storage.local.set((0, _defineProperty3.default)({}, _util.STORED_APPS_KEY, updatedStoredApps), function () { 70 | (0, _util.setElementDisplay)(applicationTable, "none"); 71 | resolve(!chrome.storage.lastError); 72 | }); 73 | }); 74 | }); 75 | }; 76 | 77 | ConnectedDomain.prototype._verifyDomainNotInUse = function (storedApps, host) { 78 | if (storedApps.length > 0) { 79 | for (var i = 0, len = storedApps.length; i < len; i++) { 80 | var app = storedApps[i]; 81 | if (app.domain === host) { 82 | return new Error('The Domain ' + _Application2.default._subColonOrUnderscore(host) + ' is already in use by another application: ' + app.name + '. Please either first disconnect ' + app.name + ' or run this application on a different domain/port.'); 83 | } 84 | } 85 | } 86 | return true; 87 | }; 88 | 89 | ConnectedDomain.prototype.disconnectDomain = function () { 90 | return this._removeDomainFromStorage(); 91 | }; 92 | 93 | /** 94 | * _removeDomainFromStorage - removes an application + domain connection from storage 95 | * 96 | * @param {String} host the host/domain of the application 97 | * @param {Array} storedApps the array of stored apps 98 | * @param {String} application the name of the application to remove 99 | * @param {Node} disconnectButton button user clicks remove an application 100 | * @return {Promise} if the removal succeeded 101 | */ 102 | ConnectedDomain.prototype._removeDomainFromStorage = function () { 103 | var _this2 = this; 104 | 105 | return new _promise2.default(function (resolve, reject) { 106 | 107 | chrome.storage.local.get(_util.STORED_APPS_KEY, function (result) { 108 | var updatedStoredApps = _this2._filterOutApp(result); 109 | 110 | chrome.storage.local.set((0, _defineProperty3.default)({}, _util.STORED_APPS_KEY, updatedStoredApps), function () { 111 | if (chrome.runtime.lastError) { 112 | reject(new Error(chrome.runtime.lastError)); 113 | } 114 | resolve(!chrome.runtime.lastError); 115 | }); 116 | }); 117 | }); 118 | }; 119 | 120 | /** 121 | * @description ConnectedDomain.prototype._filterOutApp - create a new array of connected apps that does not include the application belonging to this 122 | * 123 | * @param {Array} storedApps - connected apps in chrome storage 124 | * @return {Array} - filtered apps in chrome storage 125 | */ 126 | ConnectedDomain.prototype._filterOutApp = function (storedApps) { 127 | var _this3 = this; 128 | 129 | return storedApps[_util.STORED_APPS_KEY].filter(function (app) { 130 | return app.id !== _this3.application.id; 131 | }); 132 | }; -------------------------------------------------------------------------------- /lib/models/Application.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof2 = require("babel-runtime/helpers/typeof"); 8 | 9 | var _typeof3 = _interopRequireDefault(_typeof2); 10 | 11 | var _defineProperty2 = require("babel-runtime/helpers/defineProperty"); 12 | 13 | var _defineProperty3 = _interopRequireDefault(_defineProperty2); 14 | 15 | var _promise = require("babel-runtime/core-js/promise"); 16 | 17 | var _promise2 = _interopRequireDefault(_promise); 18 | 19 | exports.default = Application; 20 | 21 | var _util = require("../util.js"); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function Application(host, teamserverApplication) { 26 | this[host] = teamserverApplication.app_id; 27 | this.id = teamserverApplication.app_id; 28 | this.name = teamserverApplication.name; 29 | this.domain = host; 30 | this.host = host; 31 | } 32 | 33 | /** 34 | * retrieveApplicationFromStorage - get the name of an application from storage by using those host/domain name of the current tab url 35 | * 36 | * @param {Object} tab - the active tab in the active window 37 | * @return {Promise} - a connected application 38 | */ 39 | Application.retrieveApplicationFromStorage = function (tab) { 40 | return new _promise2.default(function (resolve, reject) { 41 | chrome.storage.local.get(_util.STORED_APPS_KEY, function (result) { 42 | if (chrome.runtime.lastError) { 43 | reject(new Error("Error retrieving stored applications")); 44 | } 45 | 46 | if (!result || !result[_util.STORED_APPS_KEY]) { 47 | result = (0, _defineProperty3.default)({}, _util.STORED_APPS_KEY, []); 48 | } 49 | 50 | var url = new URL(tab.url); 51 | var host = (0, _util.getHostFromUrl)(url); 52 | 53 | var application = result[_util.STORED_APPS_KEY].filter(function (app) { 54 | return app.host === host; 55 | })[0]; 56 | // application = result[STORED_APPS_KEY].filter(app => app[host])[0]; 57 | 58 | if (!application) { 59 | if (!(0, _util.isBlacklisted)(tab.url) && !chrome.runtime.lastError) { 60 | try { 61 | (0, _util.updateExtensionIcon)(tab, 1); 62 | (0, _util.updateTabBadge)(tab, ""); 63 | // updateTabBadge(tab, CONTRAST_CONFIGURE_TEXT, CONTRAST_YELLOW); 64 | } catch (e) { 65 | reject(new Error("Error updating tab badge")); 66 | } 67 | } else if ((0, _util.isBlacklisted)(tab.url) && !chrome.runtime.lastError) { 68 | try { 69 | (0, _util.updateExtensionIcon)(tab, 1); 70 | (0, _util.updateTabBadge)(tab, "", _util.CONTRAST_GREEN); 71 | } catch (e) { 72 | reject(new Error("Error updating tab badge")); 73 | } 74 | } 75 | resolve(null); 76 | } else { 77 | if (application && application.name) { 78 | var appLibText = document.getElementById('scan-libs-text'); 79 | if (appLibText) { 80 | appLibText.innerText = "Current Application:\n" + application.name.trim(); 81 | } 82 | } 83 | resolve(application); 84 | } 85 | }); 86 | }); 87 | }; 88 | 89 | /** 90 | * @description - filters an array of storedapps to get a single application 91 | * 92 | * @param {Array} storedApps - connected apps in chrome storage 93 | * @param {Object} application - an application from an Org 94 | * @return {Array} - array of 1 connected app 95 | */ 96 | Application.getStoredApp = function (appsInStorage, application) { 97 | if (!application) throw new Error("application must be defined"); 98 | 99 | if ((0, _util.isEmptyObject)(appsInStorage)) return; 100 | var storedApps = appsInStorage[_util.STORED_APPS_KEY] || appsInStorage; 101 | return storedApps.filter(function (app) { 102 | return app.id === application.app_id; 103 | })[0]; 104 | }; 105 | 106 | /** 107 | * @description - can't use a colon in app name (because objects), sub colon for an underscore. Needed when dealing with ports in app name like localhost:8080 108 | * 109 | * @param {Application/String} storedApp - the connected application 110 | * @return {String} - app domain with colon swapped in/out 111 | */ 112 | Application.subDomainColonForUnderscore = function (storedApp) { 113 | if ((typeof storedApp === "undefined" ? "undefined" : (0, _typeof3.default)(storedApp)) === "object") { 114 | return this._subColonOrUnderscore(storedApp.domain); 115 | } 116 | // storedApp is a string 117 | return this._subColonOrUnderscore(storedApp); 118 | }; 119 | 120 | /** 121 | * @description - Replaces all colons with underscores or all underscores with colons 122 | * 123 | * @param {String} string - the string to replace the characters on 124 | * @return {String} - a string with characters replaced 125 | */ 126 | Application._subColonOrUnderscore = function (string) { 127 | if (string.includes("_")) { 128 | return string.replace("_", ":"); // local dev stuff 129 | } else if (string.includes(":")) { 130 | return string.replace(":", "_"); // local dev stuff 131 | } 132 | return string; 133 | }; -------------------------------------------------------------------------------- /js/libraries/ApplicationLibrary.js: -------------------------------------------------------------------------------- 1 | import { 2 | GATHER_SCRIPTS, 3 | CONTRAST__STORED_APP_LIBS, 4 | isEmptyObject, 5 | } from '../util.js'; 6 | 7 | import Library from './Library.js'; 8 | import VulnerableApplicationLibrary from './VulnerableApplicationLibrary.js'; 9 | 10 | class ApplicationLibrary { 11 | constructor(tab, application) { 12 | this.tab = tab; 13 | this.application = application; 14 | this.libraries = []; 15 | this.STORED_APP_LIBS_ID = "APP_LIBS__ID_" + application.domain; 16 | } 17 | 18 | _setCurrentLibs(libraries) { 19 | if (libraries && Array.isArray(libraries)) { 20 | this.libraries = libraries.filter(Boolean); 21 | } 22 | } 23 | 24 | getApplicationLibraries() { 25 | return new Promise((resolve, reject) => { 26 | const { tab } = this; 27 | chrome.tabs.sendMessage(tab.id, { action: GATHER_SCRIPTS, tab }, (response) => { 28 | if (!response) { 29 | reject(new Error("No Response to GATHER_SCRIPTS")); 30 | return; 31 | } 32 | const { sharedLibraries } = response; 33 | let libraries; 34 | try { 35 | libraries = sharedLibraries.map(lib => { 36 | return (new Library(tab, lib).createVersionedLibrary()); 37 | }); 38 | } catch (e) { 39 | return; 40 | } 41 | Promise.all(libraries) // eslint-disable-line consistent-return 42 | .then(libResult => { 43 | const vulnerableApplicationLibs = libResult.map(l => { 44 | let vAL = new VulnerableApplicationLibrary(l); 45 | 46 | if (l && l.vulnerabilities && l.version) { 47 | // confident version is in app 48 | return vAL.highConfidenceVulnerability(); 49 | } 50 | // not confident version is in app 51 | return vAL.lowConfidenceVulnerabilities(); 52 | }).filter(Boolean); 53 | resolve(vulnerableApplicationLibs); 54 | }) 55 | .catch(error => { error }); 56 | }); 57 | }); 58 | } 59 | 60 | addNewApplicationLibraries(libsToAdd) { 61 | return new Promise(async(resolve) => { 62 | const { STORED_APP_LIBS_ID } = this; 63 | const libraries = await this._getStoredApplicationLibraries(); 64 | 65 | const currentLibs = libraries[CONTRAST__STORED_APP_LIBS] ? libraries[CONTRAST__STORED_APP_LIBS][STORED_APP_LIBS_ID] : null; 66 | 67 | this._setCurrentLibs(currentLibs); 68 | 69 | if (!libraries || isEmptyObject(libraries)) { 70 | libraries[CONTRAST__STORED_APP_LIBS] = {}; 71 | libraries[CONTRAST__STORED_APP_LIBS][STORED_APP_LIBS_ID] = libsToAdd; 72 | } 73 | 74 | else if (isEmptyObject(libraries[CONTRAST__STORED_APP_LIBS]) || 75 | !currentLibs || 76 | !Array.isArray(currentLibs)) { 77 | 78 | libraries[CONTRAST__STORED_APP_LIBS][STORED_APP_LIBS_ID] = libsToAdd; 79 | } 80 | 81 | else { 82 | const deDupedNewLibs = this._dedupeLibs(libsToAdd); 83 | if (deDupedNewLibs.length === 0) { 84 | resolve(null); 85 | return; 86 | } 87 | 88 | const newLibs = currentLibs.concat(deDupedNewLibs); 89 | libraries[CONTRAST__STORED_APP_LIBS][STORED_APP_LIBS_ID] = newLibs; 90 | } 91 | chrome.storage.local.set(libraries, function() { 92 | chrome.storage.local.get(CONTRAST__STORED_APP_LIBS, function(stored) { 93 | resolve(stored[CONTRAST__STORED_APP_LIBS][STORED_APP_LIBS_ID]); 94 | }); 95 | }); 96 | }); 97 | } 98 | 99 | _getStoredApplicationLibraries() { 100 | return new Promise((resolve, reject) => { 101 | chrome.storage.local.get(CONTRAST__STORED_APP_LIBS, (stored) => { 102 | if (!stored || isEmptyObject(stored)) { 103 | resolve({}); 104 | } else { 105 | resolve(stored); 106 | } 107 | reject(new Error("Stored Libs are", typeof stored)); 108 | }) 109 | }); 110 | } 111 | 112 | _dedupeLibs(newLibs) { 113 | return newLibs.filter(nL => { // filter out libs that are in storage already 114 | let filteredCurrentLibs = this.libraries.filter(cL => { 115 | if (cL.name === nL.name && nL.vulnerabilitiesCount > 1) { 116 | if (cL.vulnerabilities.length === nL.vulnerabilities.length) { 117 | return true; // no new vulnerabilities 118 | } 119 | nL.vulnerabilities = nL.vulnerabilities.filter(nLv => { 120 | return cL.vulnerabilities.filter(cLv => cLv.title !== nLv.title); 121 | }); 122 | return nL.vulnerabilities.length === 0; // no new vulnerabilities 123 | } 124 | 125 | return cL.name === nL.name; 126 | }) 127 | 128 | // if current libs contains the new libs return false and don't add the new lib 129 | return !filteredCurrentLibs[0]; 130 | }); 131 | } 132 | 133 | removeAndSetupApplicationLibraries() { 134 | if (!this.application || !this.STORED_APP_LIBS_ID) { 135 | throw new Error("Application and STORED_APP_LIBS_ID are not set."); 136 | } 137 | chrome.storage.local.remove(CONTRAST__STORED_APP_LIBS); 138 | 139 | return this._setupApplicationLibraries(); 140 | } 141 | 142 | async _setupApplicationLibraries() { 143 | const libs = await this.getApplicationLibraries(); 144 | if (!libs || libs.length === 0) { 145 | return null; 146 | } 147 | 148 | return this.addNewApplicationLibraries(libs); 149 | } 150 | 151 | 152 | } 153 | 154 | export default ApplicationLibrary; 155 | -------------------------------------------------------------------------------- /lib/libraries/Library.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _promise = require("babel-runtime/core-js/promise"); 8 | 9 | var _promise2 = _interopRequireDefault(_promise); 10 | 11 | var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck"); 12 | 13 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 14 | 15 | var _createClass2 = require("babel-runtime/helpers/createClass"); 16 | 17 | var _createClass3 = _interopRequireDefault(_createClass2); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | var Library = function () { 22 | function Library(tab, library) { 23 | (0, _classCallCheck3.default)(this, Library); 24 | 25 | this.GET_LIB_VERSION = "GET_LIB_VERSION"; 26 | this.tab = tab; 27 | this.library = library; 28 | this.extractor = null; 29 | } 30 | 31 | (0, _createClass3.default)(Library, [{ 32 | key: "_setExtrator", 33 | value: function _setExtrator(extractor) { 34 | this.extractor = extractor; 35 | } 36 | }, { 37 | key: "_setLibraryVersion", 38 | value: function _setLibraryVersion(version) { 39 | this.library.version = version; 40 | } 41 | }, { 42 | key: "createVersionedLibrary", 43 | value: function createVersionedLibrary() { 44 | var _this = this; 45 | 46 | if (this.library.extractors && this.library.extractors.func) { 47 | this._setExtrator(this.library.extractors.func[0]); 48 | return this._extractLibraryVersion(); // is a promise 49 | } 50 | return new _promise2.default(function (resolve) { 51 | return resolve(_this.library); 52 | }); 53 | } 54 | }, { 55 | key: "_extractLibraryVersion", 56 | value: function _extractLibraryVersion() { 57 | var _this2 = this; 58 | 59 | return new _promise2.default(function (resolve, reject) { 60 | var library = _this2.library, 61 | tab = _this2.tab; 62 | 63 | _this2._executeExtractionScript() 64 | // eslint-disable-next-line no-unused-vars 65 | .then(function (executed) { 66 | chrome.tabs.sendMessage(tab.id, { 67 | action: _this2.GET_LIB_VERSION, 68 | library: library 69 | }, function (version) { 70 | if (version) { 71 | _this2._setLibraryVersion(version); 72 | resolve(library); 73 | } else { 74 | _this2._setLibraryVersion(_this2._getVersionFromFileName(library.jsFileName)); 75 | resolve(library); 76 | } 77 | }); 78 | }).catch(function (error) { 79 | reject(error); 80 | }); 81 | }); 82 | } 83 | }, { 84 | key: "_getVersionFromFileName", 85 | value: function _getVersionFromFileName(jsFileName) { 86 | var version = jsFileName.match(/\b\d+(?:\.\d+)*\b/); 87 | if (version) { 88 | return version[0]; 89 | } 90 | return null; 91 | } 92 | }, { 93 | key: "_executeExtractionScript", 94 | value: function _executeExtractionScript() { 95 | var _this3 = this; 96 | 97 | return new _promise2.default(function (resolve) { 98 | var extractor = _this3.extractor, 99 | library = _this3.library, 100 | tab = _this3.tab; 101 | 102 | var details = { 103 | code: _this3._generateScriptTags({ extractor: extractor, library: library }) 104 | }; 105 | chrome.tabs.executeScript(tab.id, details, function (result) { 106 | resolve(!!result); 107 | }); 108 | }); 109 | } 110 | 111 | /** 112 | * NOTE: THIS IS NUTS 113 | * Necessary for executing a script on the webpage directly since 114 | * content scripts run in an isolated world 115 | * chrome.tabs.executeScript injects into content-script, not the page 116 | * 117 | * _generateScriptTags - Get the library version by running an extractor 118 | * function provided by Retire.js on the webpage, create an element which holds that value 119 | * 120 | * @param {Object} request request from content script 121 | * @return {String} script executed on webpage 122 | */ 123 | 124 | }, { 125 | key: "_generateScriptTags", 126 | value: function _generateScriptTags() { 127 | var extractor = this.extractor; 128 | 129 | if (!this.library.parsedLibName || !extractor) { 130 | return null; 131 | } 132 | var library = this.library.parsedLibName.replace("-", "_"); 133 | var script = "\n try {\n var _c_res" + library + " = " + extractor + ";\n var __docRes" + library + " = document.getElementById('__script_res_" + library + "');\n __docRes" + library + ".innerText = _c_res" + library + ";\n } catch (e) {\n // console.log(e) // error getting libraries\n }"; 134 | 135 | return "try {\n var script" + library + " = document.createElement('script');\n var scriptRes" + library + " = document.createElement('span');\n script" + library + ".innerHTML = `" + script + "`;\n const elId_" + library + " = '__script_res_" + library + "'\n const el_" + library + " = document.getElementById(elId_" + library + ");\n if (!el_" + library + ") {\n scriptRes" + library + ".setAttribute('id', elId_" + library + ");\n document.body.appendChild(scriptRes" + library + ");\n document.body.appendChild(script" + library + ");\n scriptRes" + library + ".style.display = 'none';\n }\n } catch (e) {}"; 136 | } 137 | }]); 138 | return Library; 139 | }(); 140 | 141 | exports.default = Library; -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | /*global 3 | chrome, 4 | document, 5 | URL, 6 | */ 7 | 8 | // import { 9 | // renderVulnerableLibraries, 10 | // } from './libraries/showLibraries.js' 11 | 12 | // import Application from './models/Application.js'; 13 | // import ApplicationLibrary from './libraries/ApplicationLibrary.js'; 14 | 15 | import { getStoredCredentials, isCredentialed } from "./util.js"; 16 | 17 | import Application from "./models/Application.js"; 18 | import ApplicationTable from "./models/ApplicationTable.js"; 19 | import Config from "./models/Config.js"; 20 | 21 | /** 22 | * indexFunction - Main function that's run, renders config button if user is on TS Organization Settings > API Page, otherwise renders vulnerability feed 23 | * 24 | * @return {void} 25 | */ 26 | export function indexFunction() { 27 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 28 | const tab = tabs[0]; 29 | const url = new URL(tab.url); 30 | getStoredCredentials() 31 | .then(async credentials => { 32 | const credentialed = isCredentialed(credentials); 33 | 34 | const application = await Application.retrieveApplicationFromStorage( 35 | tab 36 | ); 37 | // if (!application) return; 38 | 39 | const config = new Config( 40 | tab, 41 | url, 42 | credentialed, 43 | credentials, 44 | !!application 45 | ); 46 | config.addListenerToConfigButton(); 47 | config.popupScreen(); 48 | if (!credentialed) { 49 | console.log("Please Configure the Extension"); 50 | } else if (credentialed && config._isContrastPage()) { 51 | const table = new ApplicationTable(url); 52 | table.renderApplicationsMenu(); 53 | config.setGearIcon(); 54 | config.renderContrastUsername(); 55 | } else { 56 | config.setGearIcon(); 57 | config.renderContrastUsername(); 58 | if (!config._isContrastPage()) { 59 | const table = new ApplicationTable(url); 60 | table.renderActivityFeed(); 61 | if (config.hasApp) { 62 | table.renderApplicationsMenu(); 63 | } 64 | } 65 | } 66 | }) 67 | .catch(error => new Error(error)); 68 | }); 69 | } 70 | 71 | /** 72 | * Run when popup loads 73 | */ 74 | document.addEventListener("DOMContentLoaded", indexFunction, false); 75 | // document.addEventListener('DOMContentLoaded', showLibrariesButton, false); 76 | 77 | // NOTE: SAVE THE BELOW FUNCTIONS FOR LIBRARY VULNS INTEGRATION 78 | // function showLibrariesButton() { 79 | // const refreshLibsButton = document.getElementById('scan-libs-text'); 80 | // const loadingElement = document.getElementById('libs-loading'); 81 | // 82 | // chrome.tabs.query({ active: true, currentWindow: true }, async(tabs) => { 83 | // if (!tabs || tabs.length === 0) return; 84 | // const tab = tabs[0]; 85 | // const app = await Application.retrieveApplicationFromStorage(tab); 86 | // if (app) { 87 | // refreshLibsButton.classList.remove('hidden'); 88 | // refreshLibsButton.classList.add('visible'); 89 | // 90 | // addListenerToRefreshButton(refreshLibsButton, loadingElement) 91 | // } 92 | // }); 93 | // } 94 | // 95 | // function addListenerToRefreshButton(refreshLibsButton, loadingElement) { 96 | // refreshLibsButton.addEventListener('click', function() { 97 | // _renderLoadingElement(loadingElement); 98 | // chrome.tabs.query({ active: true, currentWindow: true }, async(tabs) => { 99 | // if (!tabs || tabs.length === 0) return; 100 | // const tab = tabs[0]; 101 | // const app = await Application.retrieveApplicationFromStorage(tab); 102 | // const appLib = new ApplicationLibrary(tab, app); 103 | // try { 104 | // const libs = await appLib.getApplicationLibraries(); 105 | // if (!libs || libs.length === 0) { 106 | // _renderFoundVulnerableLibraries("No libraries with vulnerabilities found."); 107 | // _hideLoadingElement(loadingElement) 108 | // return; 109 | // } 110 | // const addedLibs = await appLib.addNewApplicationLibraries(libs); 111 | // if (addedLibs && addedLibs.length > 0) { 112 | // renderVulnerableLibraries(tab, app); 113 | // _renderFoundVulnerableLibraries(`Found ${addedLibs.length} libraries with vulnerabilities.`); 114 | // _hideLoadingElement(loadingElement); 115 | // } else { 116 | // _renderFoundVulnerableLibraries("No libraries with vulnerabilities found."); 117 | // _hideLoadingElement(loadingElement); 118 | // } 119 | // } catch (e) { 120 | // _renderFoundVulnerableLibraries("Error collecting libraries."); 121 | // _hideLoadingElement(loadingElement); 122 | // } 123 | // }); 124 | // }); 125 | // } 126 | // 127 | // function _renderFoundVulnerableLibraries(message) { 128 | // const libMessage = document.getElementById('found-libs-message'); 129 | // libMessage.innerText = message; 130 | // libMessage.classList.add('visible'); 131 | // libMessage.classList.remove('hidden'); 132 | // 133 | // setTimeout(() => { 134 | // libMessage.innerText = ''; 135 | // libMessage.classList.remove('visible'); 136 | // libMessage.classList.add('hidden'); 137 | // }, 3000); 138 | // } 139 | // 140 | // function _hideLoadingElement(loadingElement) { 141 | // loadingElement.style.visibility = 'hidden'; 142 | // // setElementDisplay(loadingElement, 'none'); 143 | // } 144 | // 145 | // function _renderLoadingElement(loadingElement) { 146 | // loadingElement.style.visibility = 'visible'; 147 | // // setElementDisplay(loadingElement, 'inline'); 148 | // } 149 | -------------------------------------------------------------------------------- /scripts/murmurHash3.min.js: -------------------------------------------------------------------------------- 1 | // murmurHash3.js v2.1.2 | http://github.com/karanlyons/murmurHash.js | MIT Licensed 2 | (function(y,z){function l(a,c){return(a&65535)*c+(((a>>>16)*c&65535)<<16)}function r(a,c){return a<>>32-c}function x(a){a=l(a^a>>>16,2246822507);a^=a>>>13;a=l(a,3266489909);return a^=a>>>16}function v(a,c){a=[a[0]>>>16,a[0]&65535,a[1]>>>16,a[1]&65535];c=[c[0]>>>16,c[0]&65535,c[1]>>>16,c[1]&65535];var b=[0,0,0,0];b[3]+=a[3]+c[3];b[2]+=b[3]>>>16;b[3]&=65535;b[2]+=a[2]+c[2];b[1]+=b[2]>>>16;b[2]&=65535;b[1]+=a[1]+c[1];b[0]+=b[1]>>>16;b[1]&=65535;b[0]+=a[0]+c[0];b[0]&=65535;return[b[0]<<16|b[1],b[2]<< 3 | 16|b[3]]}function u(a,c){a=[a[0]>>>16,a[0]&65535,a[1]>>>16,a[1]&65535];c=[c[0]>>>16,c[0]&65535,c[1]>>>16,c[1]&65535];var b=[0,0,0,0];b[3]+=a[3]*c[3];b[2]+=b[3]>>>16;b[3]&=65535;b[2]+=a[2]*c[3];b[1]+=b[2]>>>16;b[2]&=65535;b[2]+=a[3]*c[2];b[1]+=b[2]>>>16;b[2]&=65535;b[1]+=a[1]*c[3];b[0]+=b[1]>>>16;b[1]&=65535;b[1]+=a[2]*c[2];b[0]+=b[1]>>>16;b[1]&=65535;b[1]+=a[3]*c[1];b[0]+=b[1]>>>16;b[1]&=65535;b[0]+=a[0]*c[3]+a[1]*c[2]+a[2]*c[1]+a[3]*c[0];b[0]&=65535;return[b[0]<<16|b[1],b[2]<<16|b[3]]}function w(a, 4 | c){c%=64;if(32===c)return[a[1],a[0]];if(32>c)return[a[0]<>>32-c,a[1]<>>32-c];c-=32;return[a[1]<>>32-c,a[0]<>>32-c]}function s(a,c){c%=64;return 0===c?a:32>c?[a[0]<>>32-c,a[1]<>>1]);a=u(a,[4283543511,3981806797]);a=p(a,[0,a[0]>>>1]);a=u(a,[3301882366,444984403]);return a=p(a,[0,a[0]>>>1])}var t={version:"2.1.2",x86:{},x64:{}};t.x86.hash32=function(a,c){a=a||"";for(var b= 5 | a.length%4,p=a.length-b,d=c||0,e=0,f=0;f>>0};t.x86.hash128=function(a,c){a=a||"";c=c||0;for(var b=a.length%16,p= 6 | a.length-b,d=c,e=c,f=c,h=c,m=0,n=0,g=0,q=0,k=0;k>>0).toString(16)).slice(-8)+("00000000"+(e>>>0).toString(16)).slice(-8)+("00000000"+(f>>>0).toString(16)).slice(-8)+("00000000"+(h>>>0).toString(16)).slice(-8)};t.x64.hash128=function(a,c){a=a||"";c=c||0;for(var b=a.length%16,l=a.length-b,d=[0,c],e=[0,c],f=[0,0],h=[0,0],m=[2277735313,289559509],n=[1291169091,658871167],g=0;g>>0).toString(16)).slice(-8)+("00000000"+(d[1]>>>0).toString(16)).slice(-8)+("00000000"+(e[0]>>>0).toString(16)).slice(-8)+ 13 | ("00000000"+(e[1]>>>0).toString(16)).slice(-8)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=t),exports.murmurHash3=t):"function"===typeof define&&define.amd?define([],function(){return t}):(t._murmurHash3=y.murmurHash3,t.noConflict=function(){y.murmurHash3=t._murmurHash3;t._murmurHash3=z;t.noConflict=z;return t},y.murmurHash3=t)})(this); 14 | -------------------------------------------------------------------------------- /dist/scripts/murmurHash3.min.js: -------------------------------------------------------------------------------- 1 | // murmurHash3.js v2.1.2 | http://github.com/karanlyons/murmurHash.js | MIT Licensed 2 | (function(y,z){function l(a,c){return(a&65535)*c+(((a>>>16)*c&65535)<<16)}function r(a,c){return a<>>32-c}function x(a){a=l(a^a>>>16,2246822507);a^=a>>>13;a=l(a,3266489909);return a^=a>>>16}function v(a,c){a=[a[0]>>>16,a[0]&65535,a[1]>>>16,a[1]&65535];c=[c[0]>>>16,c[0]&65535,c[1]>>>16,c[1]&65535];var b=[0,0,0,0];b[3]+=a[3]+c[3];b[2]+=b[3]>>>16;b[3]&=65535;b[2]+=a[2]+c[2];b[1]+=b[2]>>>16;b[2]&=65535;b[1]+=a[1]+c[1];b[0]+=b[1]>>>16;b[1]&=65535;b[0]+=a[0]+c[0];b[0]&=65535;return[b[0]<<16|b[1],b[2]<< 3 | 16|b[3]]}function u(a,c){a=[a[0]>>>16,a[0]&65535,a[1]>>>16,a[1]&65535];c=[c[0]>>>16,c[0]&65535,c[1]>>>16,c[1]&65535];var b=[0,0,0,0];b[3]+=a[3]*c[3];b[2]+=b[3]>>>16;b[3]&=65535;b[2]+=a[2]*c[3];b[1]+=b[2]>>>16;b[2]&=65535;b[2]+=a[3]*c[2];b[1]+=b[2]>>>16;b[2]&=65535;b[1]+=a[1]*c[3];b[0]+=b[1]>>>16;b[1]&=65535;b[1]+=a[2]*c[2];b[0]+=b[1]>>>16;b[1]&=65535;b[1]+=a[3]*c[1];b[0]+=b[1]>>>16;b[1]&=65535;b[0]+=a[0]*c[3]+a[1]*c[2]+a[2]*c[1]+a[3]*c[0];b[0]&=65535;return[b[0]<<16|b[1],b[2]<<16|b[3]]}function w(a, 4 | c){c%=64;if(32===c)return[a[1],a[0]];if(32>c)return[a[0]<>>32-c,a[1]<>>32-c];c-=32;return[a[1]<>>32-c,a[0]<>>32-c]}function s(a,c){c%=64;return 0===c?a:32>c?[a[0]<>>32-c,a[1]<>>1]);a=u(a,[4283543511,3981806797]);a=p(a,[0,a[0]>>>1]);a=u(a,[3301882366,444984403]);return a=p(a,[0,a[0]>>>1])}var t={version:"2.1.2",x86:{},x64:{}};t.x86.hash32=function(a,c){a=a||"";for(var b= 5 | a.length%4,p=a.length-b,d=c||0,e=0,f=0;f>>0};t.x86.hash128=function(a,c){a=a||"";c=c||0;for(var b=a.length%16,p= 6 | a.length-b,d=c,e=c,f=c,h=c,m=0,n=0,g=0,q=0,k=0;k>>0).toString(16)).slice(-8)+("00000000"+(e>>>0).toString(16)).slice(-8)+("00000000"+(f>>>0).toString(16)).slice(-8)+("00000000"+(h>>>0).toString(16)).slice(-8)};t.x64.hash128=function(a,c){a=a||"";c=c||0;for(var b=a.length%16,l=a.length-b,d=[0,c],e=[0,c],f=[0,0],h=[0,0],m=[2277735313,289559509],n=[1291169091,658871167],g=0;g>>0).toString(16)).slice(-8)+("00000000"+(d[1]>>>0).toString(16)).slice(-8)+("00000000"+(e[0]>>>0).toString(16)).slice(-8)+ 13 | ("00000000"+(e[1]>>>0).toString(16)).slice(-8)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=t),exports.murmurHash3=t):"function"===typeof define&&define.amd?define([],function(){return t}):(t._murmurHash3=y.murmurHash3,t.noConflict=function(){y.murmurHash3=t._murmurHash3;t._murmurHash3=z;t.noConflict=z;return t},y.murmurHash3=t)})(this); 14 | -------------------------------------------------------------------------------- /js/models/Vulnerability.js: -------------------------------------------------------------------------------- 1 | import { 2 | CONTRAST_RED, 3 | CONTRAST_GREEN, 4 | STORED_TRACES_KEY, 5 | HIGHLIGHT_VULNERABLE_FORMS, 6 | updateTabBadge, 7 | updateExtensionIcon, 8 | deDupeArray, 9 | generateTraceURLString, 10 | getOrganizationVulnerabilityIds, 11 | } from '../util.js'; 12 | 13 | import VulnerableTab from './VulnerableTab.js'; 14 | 15 | function Vulnerability() {} 16 | 17 | // NOTE: URL Length Maximum - https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers 18 | // NOTE: If urlQueryString length is 0 it will return all app trace ids 19 | function isTooLong(queryString) { 20 | if (queryString.length > 2000 || queryString.length === 0) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | 26 | /** 27 | * evaluateVulnerabilities - method used by tab url, xhr and form actions to check TS for vulnerabilities 28 | * 29 | * @param {Boolean} hasCredentials if the user has credentialed the extension 30 | * @param {Object} tab Gives the state of the current tab 31 | * @param {Array} traceUrls the urls that will be queried to TS 32 | * @return {void} 33 | */ 34 | Vulnerability.evaluateVulnerabilities = function(hasCredentials, tab, traceUrls, application, formTraces, resetXHRRequests) { 35 | 36 | const urlQueryString = generateTraceURLString(traceUrls); 37 | if (isTooLong(urlQueryString, traceUrls)) return; 38 | if (application && application.id) { 39 | getOrganizationVulnerabilityIds(urlQueryString, application.id) 40 | .then(json => { 41 | if (!json || !json.traces) { 42 | updateExtensionIcon(tab, 1); 43 | updateTabBadge(tab, '', CONTRAST_GREEN); 44 | throw new Error("Error getting json from app trace ids"); 45 | } 46 | // this.highlightForms(traceUrls); 47 | this.storeTraces( 48 | deDupeArray(json.traces.concat(formTraces)), 49 | tab, 50 | application, 51 | resetXHRRequests 52 | ); 53 | }) 54 | .catch((e) => { 55 | updateExtensionIcon(tab, 1); 56 | // updateTabBadge(tab, ''); 57 | updateTabBadge(tab, "!", CONTRAST_RED); 58 | console.error("Error getting organization vulnerability ids", e); 59 | }); 60 | } 61 | } 62 | 63 | Vulnerability.evaluateFormActions = async function(actions, tab, application) { 64 | if (!actions || actions.length === 0) return []; 65 | const paths = actions.map(a => (new URL(a)).pathname); 66 | const queryStrings = paths.map(path => generateTraceURLString([path])); 67 | const queries = queryStrings.map(qs => { 68 | return getOrganizationVulnerabilityIds(qs, application.id); 69 | }) 70 | const results = await Promise.all(queries); 71 | return results.map((res, i) => { 72 | res.action = actions[i] 73 | return res; 74 | }); 75 | } 76 | 77 | /** 78 | * Vulnerability - description 79 | * 80 | * @param {type} requestURL description 81 | * @param {type} tab description 82 | * @param {type} application description 83 | * @returns {type} description 84 | */ 85 | Vulnerability.evaluateSingleURL = async function(requestURL, tab, application) { 86 | const path = (new URL(requestURL)).pathname; 87 | const urlQueryString = generateTraceURLString([path]); 88 | const response = await getOrganizationVulnerabilityIds( 89 | urlQueryString, application.id); 90 | if (response && response.traces) { 91 | this.storeTraces(response.traces, tab, application); 92 | } 93 | } 94 | 95 | 96 | /** 97 | * @description - Send message to highlight vulnerable forms on DOM 98 | * 99 | * @param {type} tab description 100 | * @param {type} formActions description 101 | * @returns {type} description 102 | */ 103 | Vulnerability.highlightForms = function(tab, formActions) { 104 | chrome.tabs.sendMessage(tab.id, { 105 | action: HIGHLIGHT_VULNERABLE_FORMS, 106 | formActions, 107 | }); 108 | } 109 | 110 | /** 111 | * storeTraces - store traces associated with a tab url 112 | * 113 | * @param {Array} foundTraces - trace ids of vulnerabilities found 114 | * @param {Object} tab - Gives the state of the current tab 115 | * @return {Promise} 116 | */ 117 | Vulnerability.storeTraces = async function(traces, tab, application, resetXHRRequests = null) { 118 | const tabPath = VulnerableTab.buildTabPath(tab.url); 119 | const vulnerableTab = new VulnerableTab(tabPath, application.name, traces); 120 | let appTabs = await vulnerableTab.getStoredTab(); 121 | 122 | if (appTabs && 123 | appTabs[vulnerableTab.vulnTabId] && 124 | Array.isArray(appTabs[vulnerableTab.vulnTabId])) { 125 | 126 | const newTraces = appTabs[vulnerableTab.vulnTabId].concat(traces); 127 | vulnerableTab.setTraceIDs(newTraces); 128 | } else if (appTabs && 129 | appTabs[vulnerableTab.vulnTabId] && 130 | !Array.isArray(appTabs[vulnerableTab.vulnTabId])) { 131 | throw new Error("Vulnerabilities not stored properly, should have received array."); 132 | } 133 | 134 | appTabs = await vulnerableTab.storeTab(); 135 | 136 | try { 137 | updateExtensionIcon(tab, 0); 138 | updateTabBadge( 139 | tab, 140 | appTabs[vulnerableTab.vulnTabId].length.toString(), 141 | CONTRAST_RED 142 | ); 143 | } catch (e) { e } 144 | if (resetXHRRequests) resetXHRRequests(); 145 | } 146 | 147 | /** 148 | * removeVulnerabilitiesFromStorage - removes all trace ids from storage 149 | * 150 | * @return {Promise} - returns a promise for localhronous execution 151 | */ 152 | Vulnerability.removeVulnerabilitiesFromStorage = function() { 153 | return new Promise((resolve, reject) => { 154 | chrome.storage.local.remove(STORED_TRACES_KEY, () => { 155 | if (chrome.runtime.lastError) { 156 | reject(new Error(chrome.runtime.lastError)); 157 | } 158 | 159 | resolve(); 160 | }); 161 | }); 162 | } 163 | 164 | export default Vulnerability; 165 | -------------------------------------------------------------------------------- /js/popupMethods.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | /*global 3 | */ 4 | import { 5 | CONTRAST_ORG_UUID, 6 | TEAMSERVER_URL, 7 | SEVERITY_NOTE, 8 | SEVERITY_LOW, 9 | SEVERITY_MEDIUM, 10 | SEVERITY_HIGH, 11 | SEVERITY_CRITICAL, 12 | SEVERITY, 13 | SEVERITY_BACKGROUND_COLORS, 14 | SEVERITY_TEXT_COLORS, 15 | TRACES_REQUEST, 16 | getVulnerabilityTeamserverUrl, 17 | setElementDisplay, 18 | getVulnerabilityShort 19 | } from "./util.js"; 20 | 21 | function hideLoadingIcon() { 22 | const loading = document.getElementById("vulns-loading"); 23 | loading.style.display = "none"; 24 | } 25 | 26 | /** 27 | * populateVulnerabilitySection - Get details about each trace from teamserver and then render each of them as a list item in the extension popup 28 | * 29 | * @param {Array} traces Trace uuids sent to teamserver for more info 30 | * @param {String} teamserverUrl The url of the TS environment we're using 31 | * @param {String} orgUuid The uuid of our org 32 | * @return {void} Renders a list of vulnerabilities 33 | */ 34 | function populateVulnerabilitySection( 35 | traces, 36 | teamserverUrl, 37 | orgUuid, 38 | application 39 | ) { 40 | if (traces.length > 0) { 41 | // NOTE: Elements set to display show in getStorageVulnsAndRender 42 | getShortVulnerabilities(traces, application.id) 43 | .then(sortedTraces => { 44 | hideLoadingIcon(); 45 | sortedTraces.map(trace => { 46 | return renderListItem(trace, teamserverUrl, orgUuid, application); 47 | }); 48 | }) 49 | .catch(new Error("Error rendering sorted traces into list items.")); 50 | } 51 | } 52 | 53 | function getShortVulnerabilities(traces, appID) { 54 | return Promise.all(traces.map(t => getVulnerabilityShort(t, appID))) 55 | .then(shortTraces => { 56 | return shortTraces.map(t => t.trace).sort((a, b) => { 57 | return SEVERITY[b.severity] - SEVERITY[a.severity]; 58 | }); 59 | }) 60 | .catch(new Error("Error getting and rendering vulnerabilities")); 61 | } 62 | 63 | /** 64 | * renderListItem - Renders details about a vulnerability as a list item 65 | * 66 | * @param {Object} shortTrace Details about a vulnerability 67 | * @param {String} teamserverUrl The url of the TS environment we're using 68 | * @param {String} orgUuid The uuid of our org 69 | * @return {void} a new list item 70 | */ 71 | function renderListItem(trace, teamserverUrl, orgUuid) { 72 | if (!trace) return; 73 | 74 | let ul = document.getElementById("vulnerabilities-found-on-page-list"); 75 | let li = document.createElement("li"); 76 | li.classList.add("no-border"); 77 | li.classList.add("vulnerability-li"); 78 | 79 | switch (trace.severity) { 80 | case SEVERITY_NOTE: 81 | createBadge(SEVERITY_NOTE, li); 82 | break; 83 | case SEVERITY_LOW: 84 | createBadge(SEVERITY_LOW, li); 85 | break; 86 | case SEVERITY_MEDIUM: 87 | createBadge(SEVERITY_MEDIUM, li); 88 | break; 89 | case SEVERITY_HIGH: 90 | createBadge(SEVERITY_HIGH, li); 91 | break; 92 | case SEVERITY_CRITICAL: 93 | createBadge(SEVERITY_CRITICAL, li); 94 | break; 95 | default: 96 | break; 97 | } 98 | 99 | // Teamserver returns camelCase vs snake_case depending on endpoint 100 | // const ruleName = trace.ruleName || trace.rule_name; 101 | 102 | const anchor = document.createElement("a"); 103 | anchor.classList.add("vulnerability-link"); 104 | anchor.classList.add("vulnerability-rule-name"); 105 | anchor.innerText = trace.title; //" " + ruleName.split('-').join(' ').titleize(); 106 | anchor.onclick = function() { 107 | chrome.tabs.create({ 108 | url: getVulnerabilityTeamserverUrl(teamserverUrl, orgUuid, trace.uuid), 109 | active: false 110 | }); 111 | }; 112 | li.appendChild(anchor); 113 | 114 | // append li last to load content smootly (is the way it works?) 115 | ul.appendChild(li); 116 | } 117 | 118 | /** 119 | * getStorageVulnsAndRender - gets stored traces from background, renders the vulnerability section of the popup and sends the vulnerabilities to populateVulnerabilitySection for rendering into a list 120 | * 121 | * @param {Object} items - credentials 122 | * @return {void} 123 | */ 124 | function getStorageVulnsAndRender(items, application, tab) { 125 | const noVulnsFound = document.getElementById("no-vulnerabilities-found"); 126 | const vulnsOnPage = document.getElementById("vulnerabilities-found-on-page"); 127 | chrome.runtime.sendMessage( 128 | { action: TRACES_REQUEST, application, tab }, 129 | response => { 130 | if (response && response.traces && response.traces.length > 0) { 131 | setElementDisplay(noVulnsFound, "none"); 132 | setElementDisplay(vulnsOnPage, "block"); 133 | 134 | populateVulnerabilitySection( 135 | response.traces, 136 | items[TEAMSERVER_URL], 137 | items[CONTRAST_ORG_UUID], 138 | application 139 | ); 140 | } else { 141 | hideLoadingIcon(); 142 | setElementDisplay(noVulnsFound, "block"); 143 | setElementDisplay(vulnsOnPage, "none"); 144 | } 145 | } 146 | ); 147 | } 148 | 149 | const createBadge = (severity, li) => { 150 | let parent = document.createElement("div"); 151 | parent.classList.add("parent-badge"); 152 | 153 | let child = document.createElement("div"); 154 | child.classList.add("child-badge"); 155 | child.innerText = severity; 156 | child.style.color = SEVERITY_TEXT_COLORS[severity]; 157 | 158 | parent.style.backgroundColor = SEVERITY_BACKGROUND_COLORS[severity]; 159 | 160 | parent.appendChild(child); 161 | li.appendChild(parent); 162 | }; 163 | 164 | export { 165 | hideLoadingIcon, 166 | populateVulnerabilitySection, 167 | renderListItem, 168 | getStorageVulnsAndRender, 169 | getShortVulnerabilities 170 | }; 171 | -------------------------------------------------------------------------------- /js/libraries/showLibraries.js: -------------------------------------------------------------------------------- 1 | import { 2 | SEVERITY, 3 | SEVERITY_LOW, 4 | SEVERITY_MEDIUM, 5 | SEVERITY_HIGH, 6 | CONTRAST__STORED_APP_LIBS, 7 | SEVERITY_BACKGROUND_COLORS, 8 | SEVERITY_TEXT_COLORS, 9 | isEmptyObject, 10 | capitalize, 11 | } from '../util.js'; 12 | 13 | import Application from '../models/Application.js'; 14 | 15 | const getLibrariesFromStorage = (tab, application) => { 16 | return new Promise((resolve, reject) => { 17 | const appKey = "APP_LIBS__ID_" + application.domain; 18 | chrome.storage.local.get(CONTRAST__STORED_APP_LIBS, (result) => { 19 | if (isEmptyObject(result)) { 20 | resolve(null); 21 | } else { 22 | const libraries = result[CONTRAST__STORED_APP_LIBS][appKey]; 23 | resolve(libraries); 24 | } 25 | reject(new Error("result was", typeof result)); 26 | }); 27 | }); 28 | } 29 | 30 | const _getTabAndApplication = () => { 31 | return new Promise((resolve, reject) => { 32 | chrome.tabs.query({ active: true, currentWindow: true }, async(tabs) => { 33 | const tab = tabs[0]; 34 | if (!tab) { 35 | reject(new Error("Tab is null")); 36 | return; 37 | } 38 | const application = await Application.retrieveApplicationFromStorage(tab); 39 | resolve({ tab, application }); 40 | }); 41 | }); 42 | } 43 | 44 | const renderVulnerableLibraries = async(tab, application) => { 45 | if (!tab || !application) { 46 | const tabAndApp = await _getTabAndApplication(); 47 | tab = tabAndApp.tab; 48 | application = tabAndApp.application; 49 | } 50 | let libraries = await getLibrariesFromStorage(tab, application); 51 | 52 | if (!libraries || libraries.length === 0) return; 53 | 54 | const container = document.getElementById('libs-vulnerabilities-found-on-page'); 55 | const ul = document.getElementById('libs-vulnerabilities-found-on-page-list'); 56 | 57 | libraries = libraries.sort((a, b) => { 58 | if (!a.severity && b.severity) { 59 | return 1; 60 | } else if (a.severity && !b.severity) { 61 | return -1; 62 | } else if (!a.severity && !b.severity) { 63 | return 0; 64 | } 65 | return SEVERITY[b.severity.titleize()] - SEVERITY[a.severity.titleize()]; 66 | }); 67 | 68 | let listItemTexts = []; 69 | for (let i = 0, len = libraries.length; i < len; i++) { 70 | let lib = libraries[i]; 71 | if (!lib) continue; 72 | for (let j = 0; j < lib.vulnerabilitiesCount; j++) { 73 | if (!lib.vulnerabilities) continue; 74 | let vulnObj = lib.vulnerabilities[j]; 75 | vulnObj.title = _vulnObjTitle(vulnObj); 76 | // vulnObj.version = _setVulnerabilityVersion(vulnObj); 77 | let name = vulnObj.name || lib.name; 78 | if (!listItemTexts.includes(vulnObj.title + name)) { 79 | _createVulnerabilityListItem(ul, lib.name, vulnObj); 80 | listItemTexts.push(vulnObj.title + name); 81 | } 82 | } 83 | } 84 | 85 | container.classList.remove('hidden'); 86 | container.classList.add('visible'); 87 | } 88 | 89 | const _vulnObjTitle = (vulnObj) => { 90 | let title = vulnObj.title; 91 | if (!title) { 92 | title = vulnObj.identifiers; 93 | if (typeof title !== 'string') { 94 | return title.summary; 95 | } 96 | return title; 97 | } 98 | return title; 99 | } 100 | 101 | // NOTE: Leave for now, not sure if version should be included 102 | // 103 | // const versionTypes = { 104 | // atOrAbove: ">=", 105 | // atOrBelow: "<=", 106 | // below: "<", 107 | // above: ">", 108 | // } 109 | // 110 | // const _setVulnerabilityVersion = (vulnObj) => { 111 | // let versions = vulnObj.versions || vulnObj; 112 | // let version = []; 113 | // try { 114 | // let keys = Object.keys(versions); 115 | // let vals = Object.values(versions); 116 | // for (let k = keys.length, kLen = -1; k > kLen; k--) { 117 | // if (versionTypes[keys[k]]) { 118 | // version.push( 119 | // `${versionTypes[keys[k]]} ${vals[k]}`); 120 | // } 121 | // } 122 | // } catch (e) { e } 123 | // 124 | // if (version.length > 1) { 125 | // version = version.join(" and "); 126 | // } else { 127 | // version = version[0]; 128 | // } 129 | // return version; 130 | // } 131 | 132 | const createBadge = (severity, li) => { 133 | let parent = document.createElement('div'); 134 | parent.classList.add('parent-badge'); 135 | 136 | let child = document.createElement('div'); 137 | child.classList.add('child-badge'); 138 | child.innerText = severity; 139 | child.style.color = SEVERITY_TEXT_COLORS[severity]; 140 | 141 | parent.style.backgroundColor = SEVERITY_BACKGROUND_COLORS[severity]; 142 | 143 | parent.appendChild(child); 144 | li.appendChild(parent); 145 | } 146 | 147 | const _createVulnerabilityListItem = (ul, libName, vulnObj) => { 148 | let { name, severity, title, link } = vulnObj; 149 | if (!name) { 150 | name = libName; 151 | name = name.titleize(); 152 | } 153 | 154 | let li = document.createElement('li'); 155 | li.classList.add('list-group-item'); 156 | li.classList.add('no-border'); 157 | li.classList.add('vulnerability-li'); 158 | 159 | switch (severity.toLowerCase()) { 160 | case SEVERITY_LOW.toLowerCase(): { 161 | createBadge(SEVERITY_LOW, li); 162 | break; 163 | } 164 | case SEVERITY_MEDIUM.toLowerCase(): { 165 | createBadge(SEVERITY_MEDIUM, li); 166 | break; 167 | } 168 | case SEVERITY_HIGH.toLowerCase(): { 169 | createBadge(SEVERITY_HIGH, li); 170 | break; 171 | } 172 | default: 173 | break; 174 | } 175 | 176 | name = name.replace('Jquery', 'JQuery'); 177 | 178 | let anchor = document.createElement('a'); 179 | anchor.classList.add('vulnerability-link'); 180 | anchor.classList.add('vulnerability-rule-name'); 181 | anchor.innerText = name + ": " + capitalize(title.trim()); // + version 182 | anchor.onclick = function() { 183 | chrome.tabs.create({ 184 | url: link, 185 | active: false 186 | }); 187 | } 188 | li.appendChild(anchor); 189 | 190 | ul.appendChild(li); 191 | } 192 | 193 | export { 194 | renderVulnerableLibraries, 195 | } 196 | -------------------------------------------------------------------------------- /lib/popupMethods.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.getShortVulnerabilities = exports.getStorageVulnsAndRender = exports.renderListItem = exports.populateVulnerabilitySection = exports.hideLoadingIcon = undefined; 7 | 8 | var _promise = require("babel-runtime/core-js/promise"); 9 | 10 | var _promise2 = _interopRequireDefault(_promise); 11 | 12 | var _util = require("./util.js"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | function hideLoadingIcon() { 17 | var loading = document.getElementById("vulns-loading"); 18 | loading.style.display = "none"; 19 | } 20 | 21 | /** 22 | * populateVulnerabilitySection - Get details about each trace from teamserver and then render each of them as a list item in the extension popup 23 | * 24 | * @param {Array} traces Trace uuids sent to teamserver for more info 25 | * @param {String} teamserverUrl The url of the TS environment we're using 26 | * @param {String} orgUuid The uuid of our org 27 | * @return {void} Renders a list of vulnerabilities 28 | */ 29 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 30 | /*global 31 | */ 32 | function populateVulnerabilitySection(traces, teamserverUrl, orgUuid, application) { 33 | if (traces.length > 0) { 34 | // NOTE: Elements set to display show in getStorageVulnsAndRender 35 | getShortVulnerabilities(traces, application.id).then(function (sortedTraces) { 36 | hideLoadingIcon(); 37 | sortedTraces.map(function (trace) { 38 | return renderListItem(trace, teamserverUrl, orgUuid, application); 39 | }); 40 | }).catch(new Error("Error rendering sorted traces into list items.")); 41 | } 42 | } 43 | 44 | function getShortVulnerabilities(traces, appID) { 45 | return _promise2.default.all(traces.map(function (t) { 46 | return (0, _util.getVulnerabilityShort)(t, appID); 47 | })).then(function (shortTraces) { 48 | return shortTraces.map(function (t) { 49 | return t.trace; 50 | }).sort(function (a, b) { 51 | return _util.SEVERITY[b.severity] - _util.SEVERITY[a.severity]; 52 | }); 53 | }).catch(new Error("Error getting and rendering vulnerabilities")); 54 | } 55 | 56 | /** 57 | * renderListItem - Renders details about a vulnerability as a list item 58 | * 59 | * @param {Object} shortTrace Details about a vulnerability 60 | * @param {String} teamserverUrl The url of the TS environment we're using 61 | * @param {String} orgUuid The uuid of our org 62 | * @return {void} a new list item 63 | */ 64 | function renderListItem(trace, teamserverUrl, orgUuid) { 65 | if (!trace) return; 66 | 67 | var ul = document.getElementById("vulnerabilities-found-on-page-list"); 68 | var li = document.createElement("li"); 69 | li.classList.add("no-border"); 70 | li.classList.add("vulnerability-li"); 71 | 72 | switch (trace.severity) { 73 | case _util.SEVERITY_NOTE: 74 | createBadge(_util.SEVERITY_NOTE, li); 75 | break; 76 | case _util.SEVERITY_LOW: 77 | createBadge(_util.SEVERITY_LOW, li); 78 | break; 79 | case _util.SEVERITY_MEDIUM: 80 | createBadge(_util.SEVERITY_MEDIUM, li); 81 | break; 82 | case _util.SEVERITY_HIGH: 83 | createBadge(_util.SEVERITY_HIGH, li); 84 | break; 85 | case _util.SEVERITY_CRITICAL: 86 | createBadge(_util.SEVERITY_CRITICAL, li); 87 | break; 88 | default: 89 | break; 90 | } 91 | 92 | // Teamserver returns camelCase vs snake_case depending on endpoint 93 | // const ruleName = trace.ruleName || trace.rule_name; 94 | 95 | var anchor = document.createElement("a"); 96 | anchor.classList.add("vulnerability-link"); 97 | anchor.classList.add("vulnerability-rule-name"); 98 | anchor.innerText = trace.title; //" " + ruleName.split('-').join(' ').titleize(); 99 | anchor.onclick = function () { 100 | chrome.tabs.create({ 101 | url: (0, _util.getVulnerabilityTeamserverUrl)(teamserverUrl, orgUuid, trace.uuid), 102 | active: false 103 | }); 104 | }; 105 | li.appendChild(anchor); 106 | 107 | // append li last to load content smootly (is the way it works?) 108 | ul.appendChild(li); 109 | } 110 | 111 | /** 112 | * getStorageVulnsAndRender - gets stored traces from background, renders the vulnerability section of the popup and sends the vulnerabilities to populateVulnerabilitySection for rendering into a list 113 | * 114 | * @param {Object} items - credentials 115 | * @return {void} 116 | */ 117 | function getStorageVulnsAndRender(items, application, tab) { 118 | var noVulnsFound = document.getElementById("no-vulnerabilities-found"); 119 | var vulnsOnPage = document.getElementById("vulnerabilities-found-on-page"); 120 | chrome.runtime.sendMessage({ action: _util.TRACES_REQUEST, application: application, tab: tab }, function (response) { 121 | if (response && response.traces && response.traces.length > 0) { 122 | (0, _util.setElementDisplay)(noVulnsFound, "none"); 123 | (0, _util.setElementDisplay)(vulnsOnPage, "block"); 124 | 125 | populateVulnerabilitySection(response.traces, items[_util.TEAMSERVER_URL], items[_util.CONTRAST_ORG_UUID], application); 126 | } else { 127 | hideLoadingIcon(); 128 | (0, _util.setElementDisplay)(noVulnsFound, "block"); 129 | (0, _util.setElementDisplay)(vulnsOnPage, "none"); 130 | } 131 | }); 132 | } 133 | 134 | var createBadge = function createBadge(severity, li) { 135 | var parent = document.createElement("div"); 136 | parent.classList.add("parent-badge"); 137 | 138 | var child = document.createElement("div"); 139 | child.classList.add("child-badge"); 140 | child.innerText = severity; 141 | child.style.color = _util.SEVERITY_TEXT_COLORS[severity]; 142 | 143 | parent.style.backgroundColor = _util.SEVERITY_BACKGROUND_COLORS[severity]; 144 | 145 | parent.appendChild(child); 146 | li.appendChild(parent); 147 | }; 148 | 149 | exports.hideLoadingIcon = hideLoadingIcon; 150 | exports.populateVulnerabilitySection = populateVulnerabilitySection; 151 | exports.renderListItem = renderListItem; 152 | exports.getStorageVulnsAndRender = getStorageVulnsAndRender; 153 | exports.getShortVulnerabilities = getShortVulnerabilities; -------------------------------------------------------------------------------- /lib/content-scripts/ContrastForms.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | /*global 3 | chrome, 4 | document, 5 | getHostFromUrl, 6 | deDupeArray, 7 | STORED_APPS_KEY, 8 | GATHER_FORMS_ACTION, 9 | CONTRAST_GREEN, 10 | */ 11 | 12 | "use strict"; 13 | 14 | function ContrastForm(forms) { 15 | this.forms = forms; 16 | } 17 | 18 | ContrastForm.MESSAGE_SENT = false; 19 | 20 | /** 21 | * extractActionsFromForm - gets the form actions from each form in a collection 22 | * 23 | * @param {HTMLCollection} forms collection of forms extracted from DOM 24 | * @return {Array} array of form actions 25 | */ 26 | ContrastForm._extractActionsFromForm = function (forms) { 27 | var actions = []; 28 | for (var i = 0; i < forms.length; i++) { 29 | var form = forms[i]; 30 | var conditions = [form, !!form.action && form.action.length > 0]; 31 | if (conditions.every(Boolean)) { 32 | actions.push(form.action); 33 | } 34 | } 35 | return actions; 36 | }; 37 | 38 | /** 39 | * collectFormActions - scrapes DOM for forms and collects their actions, uses a mutation observer for SPAs and only for a connected application 40 | * 41 | * @return {void} 42 | */ 43 | ContrastForm.collectFormActions = function (sendResponse) { 44 | var _this = this; 45 | 46 | chrome.storage.local.get(STORED_APPS_KEY, function (result) { 47 | if (chrome.runtime.lastError) return; 48 | if (!result || !result[STORED_APPS_KEY]) return; 49 | 50 | var url = new URL(window.location.href); 51 | var host = getHostFromUrl(url); 52 | var application = result[STORED_APPS_KEY].filter(function (app) { 53 | return app.host === host; 54 | })[0]; 55 | 56 | if (!application) return; 57 | 58 | // don't run this when page has been refreshed, rely on mutation observer instead, use === false to prevent running on undefine 59 | var actions = _this._scrapeDOMForForms() || []; 60 | _this.MESSAGE_SENT = true; 61 | _this._sendFormActionsToBackground(actions, sendResponse); 62 | return; 63 | }); 64 | }; 65 | 66 | /** 67 | * 68 | */ 69 | ContrastForm._collectMutatedForms = function (mutations, observer, sendResponse) { 70 | var formActions = this._getFormsFromMutations(mutations); 71 | 72 | // send formActions to background and stop observation 73 | if (formActions.length > 0) { 74 | ContrastForm.MESSAGE_SENT = true; 75 | this._sendFormActionsToBackground(formActions, sendResponse); 76 | window.CONTRAST__REFRESHED = false; 77 | observer.disconnect(); 78 | } 79 | }.bind(ContrastForm); 80 | 81 | /** 82 | * ContrastForm - description 83 | * 84 | * @param {HTMLCollection} mutations - elements that have mutated 85 | * @return {Array} - actions on forms 86 | */ 87 | ContrastForm._getFormsFromMutations = function (mutations) { 88 | var _this2 = this; 89 | 90 | var formActions = []; 91 | 92 | // go through each mutation, looking for elements that have changed in a specific manner 93 | return mutations.map(function (mutation) { 94 | var mutatedForms = void 0; 95 | if (mutation.target.tagName === "FORM") { 96 | mutatedForms = mutation.target; 97 | } else { 98 | mutatedForms = mutation.target.getElementsByTagName("form"); 99 | } 100 | 101 | // if the mutated element has child forms 102 | if (!!mutatedForms && mutatedForms.length > 0) { 103 | var actions = _this2._extractActionsFromForm(mutatedForms); 104 | return formActions.concat(actions); 105 | } 106 | return null; 107 | }).filter(Boolean); 108 | }; 109 | 110 | /** 111 | * _scrapeDOMForForms - retrieve forms directly from the DOM 112 | * 113 | * @return {Array} - an array of actions from forms 114 | */ 115 | ContrastForm._scrapeDOMForForms = function () { 116 | var forms = []; 117 | var formActions = []; 118 | var domForms = document.getElementsByTagName("form"); 119 | for (var i = 0; i < domForms.length; i++) { 120 | // highlightForm(domForms[i]) 121 | // only collect forms that are shown on DOM 122 | // don't use `.splice()` because that mutates array we're running loop on 123 | if (!parentHasDisplayNone(domForms[i])) { 124 | forms.push(domForms[i]); 125 | } 126 | } 127 | if (forms.length > 0) { 128 | formActions = formActions.concat(this._extractActionsFromForm(forms)); 129 | } 130 | return formActions; 131 | }; 132 | 133 | /** 134 | * _sendFormActionsToBackground - sends the array for form actions to background 135 | * 136 | * @param {Array} formActions - actions from forms, scraped from DOM 137 | * @return {void} 138 | */ 139 | ContrastForm._sendFormActionsToBackground = function (formActions, sendResponse) { 140 | sendResponse({ 141 | sender: GATHER_FORMS_ACTION, 142 | formActions: deDupeArray(formActions.flatten()) 143 | }); 144 | }; 145 | 146 | /** 147 | * highlightForm - description 148 | * 149 | * @param {Node} form - A form on the DOM 150 | * @return {void} 151 | */ 152 | ContrastForm.highlightForms = function (traceUrls) { 153 | // only want the trace path names 154 | // traceUrls = traceUrls.map(t => new URL(t).pathname) 155 | if (!traceUrls || traceUrls.length === 0) return false; 156 | 157 | var forms = document.getElementsByTagName("form"); 158 | for (var i = 0; i < forms.length; i++) { 159 | var form = forms[i]; 160 | 161 | // webgoat makes loading forms interesting, so we need to go up the DOM tree and verify that the form, and the form's parents are displayed 162 | if (traceUrls.includes(form.action) && !parentHasDisplayNone(form)) { 163 | if (this._highlightSubmitInput(form)) { 164 | return true; 165 | } 166 | } 167 | } 168 | return false; 169 | }; 170 | 171 | /** 172 | * ContrastForm - description 173 | * 174 | * @param {HTMLNode} form - form to highlight 175 | * @return {Boolean} 176 | */ 177 | ContrastForm._highlightSubmitInput = function (form) { 178 | var inputs = form.getElementsByTagName("input"); 179 | var input = void 0; 180 | for (var j = 0, len = inputs.length; j < len; j++) { 181 | if (!input && inputs[j].type === "submit") { 182 | input = inputs[j]; 183 | } 184 | } 185 | 186 | // highlight with contrast aquamarine color 187 | if (input) { 188 | input.setAttribute("style", "border-radius: 5px;\n border: 3px solid " + CONTRAST_GREEN); 189 | 190 | return true; 191 | } 192 | return false; 193 | }; 194 | 195 | /** 196 | * parentHasDisplayNone - checks if any parent elements of a node has display: none styling 197 | * 198 | * @param {Node} element an HTML element 199 | * @return {Boolean} if that element has a parent with display: none 200 | */ 201 | function parentHasDisplayNone(element) { 202 | while (element.tagName !== "BODY") { 203 | if (element.style.display === "none") return true; 204 | element = element.parentNode; 205 | } 206 | return false; 207 | } -------------------------------------------------------------------------------- /js/content-scripts/ContrastForms.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | /*global 3 | chrome, 4 | document, 5 | getHostFromUrl, 6 | deDupeArray, 7 | STORED_APPS_KEY, 8 | GATHER_FORMS_ACTION, 9 | CONTRAST_GREEN, 10 | */ 11 | 12 | "use strict"; 13 | 14 | function ContrastForm(forms) { 15 | this.forms = forms; 16 | } 17 | 18 | ContrastForm.MESSAGE_SENT = false; 19 | 20 | /** 21 | * extractActionsFromForm - gets the form actions from each form in a collection 22 | * 23 | * @param {HTMLCollection} forms collection of forms extracted from DOM 24 | * @return {Array} array of form actions 25 | */ 26 | ContrastForm._extractActionsFromForm = function(forms) { 27 | let actions = []; 28 | for (let i = 0; i < forms.length; i++) { 29 | let form = forms[i]; 30 | let conditions = [form, !!form.action && form.action.length > 0]; 31 | if (conditions.every(Boolean)) { 32 | actions.push(form.action); 33 | } 34 | } 35 | return actions; 36 | }; 37 | 38 | /** 39 | * collectFormActions - scrapes DOM for forms and collects their actions, uses a mutation observer for SPAs and only for a connected application 40 | * 41 | * @return {void} 42 | */ 43 | ContrastForm.collectFormActions = function(sendResponse) { 44 | chrome.storage.local.get(STORED_APPS_KEY, result => { 45 | if (chrome.runtime.lastError) return; 46 | if (!result || !result[STORED_APPS_KEY]) return; 47 | 48 | const url = new URL(window.location.href); 49 | const host = getHostFromUrl(url); 50 | const application = result[STORED_APPS_KEY].filter( 51 | app => app.host === host 52 | )[0]; 53 | 54 | if (!application) return; 55 | 56 | // don't run this when page has been refreshed, rely on mutation observer instead, use === false to prevent running on undefine 57 | const actions = this._scrapeDOMForForms() || []; 58 | this.MESSAGE_SENT = true; 59 | this._sendFormActionsToBackground(actions, sendResponse); 60 | return; 61 | }); 62 | }; 63 | 64 | /** 65 | * 66 | */ 67 | ContrastForm._collectMutatedForms = function( 68 | mutations, 69 | observer, 70 | sendResponse 71 | ) { 72 | const formActions = this._getFormsFromMutations(mutations); 73 | 74 | // send formActions to background and stop observation 75 | if (formActions.length > 0) { 76 | ContrastForm.MESSAGE_SENT = true; 77 | this._sendFormActionsToBackground(formActions, sendResponse); 78 | window.CONTRAST__REFRESHED = false; 79 | observer.disconnect(); 80 | } 81 | }.bind(ContrastForm); 82 | 83 | /** 84 | * ContrastForm - description 85 | * 86 | * @param {HTMLCollection} mutations - elements that have mutated 87 | * @return {Array} - actions on forms 88 | */ 89 | ContrastForm._getFormsFromMutations = function(mutations) { 90 | let formActions = []; 91 | 92 | // go through each mutation, looking for elements that have changed in a specific manner 93 | return mutations 94 | .map(mutation => { 95 | let mutatedForms; 96 | if (mutation.target.tagName === "FORM") { 97 | mutatedForms = mutation.target; 98 | } else { 99 | mutatedForms = mutation.target.getElementsByTagName("form"); 100 | } 101 | 102 | // if the mutated element has child forms 103 | if (!!mutatedForms && mutatedForms.length > 0) { 104 | let actions = this._extractActionsFromForm(mutatedForms); 105 | return formActions.concat(actions); 106 | } 107 | return null; 108 | }) 109 | .filter(Boolean); 110 | }; 111 | 112 | /** 113 | * _scrapeDOMForForms - retrieve forms directly from the DOM 114 | * 115 | * @return {Array} - an array of actions from forms 116 | */ 117 | ContrastForm._scrapeDOMForForms = function() { 118 | let forms = []; 119 | let formActions = []; 120 | let domForms = document.getElementsByTagName("form"); 121 | for (let i = 0; i < domForms.length; i++) { 122 | // highlightForm(domForms[i]) 123 | // only collect forms that are shown on DOM 124 | // don't use `.splice()` because that mutates array we're running loop on 125 | if (!parentHasDisplayNone(domForms[i])) { 126 | forms.push(domForms[i]); 127 | } 128 | } 129 | if (forms.length > 0) { 130 | formActions = formActions.concat(this._extractActionsFromForm(forms)); 131 | } 132 | return formActions; 133 | }; 134 | 135 | /** 136 | * _sendFormActionsToBackground - sends the array for form actions to background 137 | * 138 | * @param {Array} formActions - actions from forms, scraped from DOM 139 | * @return {void} 140 | */ 141 | ContrastForm._sendFormActionsToBackground = function( 142 | formActions, 143 | sendResponse 144 | ) { 145 | sendResponse({ 146 | sender: GATHER_FORMS_ACTION, 147 | formActions: deDupeArray(formActions.flatten()) 148 | }); 149 | }; 150 | 151 | /** 152 | * highlightForm - description 153 | * 154 | * @param {Node} form - A form on the DOM 155 | * @return {void} 156 | */ 157 | ContrastForm.highlightForms = function(traceUrls) { 158 | // only want the trace path names 159 | // traceUrls = traceUrls.map(t => new URL(t).pathname) 160 | if (!traceUrls || traceUrls.length === 0) return false; 161 | 162 | const forms = document.getElementsByTagName("form"); 163 | for (let i = 0; i < forms.length; i++) { 164 | let form = forms[i]; 165 | 166 | // webgoat makes loading forms interesting, so we need to go up the DOM tree and verify that the form, and the form's parents are displayed 167 | if (traceUrls.includes(form.action) && !parentHasDisplayNone(form)) { 168 | if (this._highlightSubmitInput(form)) { 169 | return true; 170 | } 171 | } 172 | } 173 | return false; 174 | }; 175 | 176 | /** 177 | * ContrastForm - description 178 | * 179 | * @param {HTMLNode} form - form to highlight 180 | * @return {Boolean} 181 | */ 182 | ContrastForm._highlightSubmitInput = function(form) { 183 | let inputs = form.getElementsByTagName("input"); 184 | let input; 185 | for (let j = 0, len = inputs.length; j < len; j++) { 186 | if (!input && inputs[j].type === "submit") { 187 | input = inputs[j]; 188 | } 189 | } 190 | 191 | // highlight with contrast aquamarine color 192 | if (input) { 193 | input.setAttribute( 194 | "style", 195 | `border-radius: 5px; 196 | border: 3px solid ${CONTRAST_GREEN}` 197 | ); 198 | 199 | return true; 200 | } 201 | return false; 202 | }; 203 | 204 | /** 205 | * parentHasDisplayNone - checks if any parent elements of a node has display: none styling 206 | * 207 | * @param {Node} element an HTML element 208 | * @return {Boolean} if that element has a parent with display: none 209 | */ 210 | function parentHasDisplayNone(element) { 211 | while (element.tagName !== "BODY") { 212 | if (element.style.display === "none") return true; 213 | element = element.parentNode; 214 | } 215 | return false; 216 | } 217 | -------------------------------------------------------------------------------- /js/models/PopupTableRow.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 2 | import { 3 | CONTRAST_RED, 4 | CONTRAST_GREEN, 5 | setElementText, 6 | setElementDisplay, 7 | changeElementVisibility, 8 | APPLICATION_CONNECTED 9 | } from "../util.js"; 10 | 11 | import Application from "./Application.js"; 12 | import ConnectedDomain from "./ConnectedDomain.js"; 13 | 14 | const HOST_SPAN_CLASS = "app-host-span"; 15 | const CONNECT_BUTTON_TEXT = "Connect"; 16 | const CONNECT_SUCCESS_MESSAGE = 17 | "Successfully connected. Please reload the page."; 18 | const CONNECT_FAILURE_MESSAGE = "Error connecting. Try refreshing the page."; 19 | // const DISCONNECT_SUCCESS_MESSAGE = "Successfully Disconnected"; 20 | const DISCONNECT_FAILURE_MESSAGE = "Error Disconnecting"; 21 | const DISCONNECT_BUTTON_TEXT = "Disconnect"; 22 | 23 | const CONTRAST_BUTTON_CLASS = 24 | "btn btn-primary btn-xs btn-contrast-plugin btn-connect"; 25 | const CONTRAST_BUTTON_DISCONNECT_CLASS = 26 | "btn btn-primary btn-xs btn-contrast-plugin btn-disconnect"; 27 | 28 | export default function TableRow(application, url, table) { 29 | this.application = application; 30 | this.url = url; 31 | this.table = table; 32 | this.host = ""; 33 | this.row = document.createElement("tr"); 34 | this.nameTD = document.createElement("td"); 35 | // this.appIdTD = document.createElement('td'); 36 | this.buttonTD = document.createElement("td"); 37 | // this.disconnectTD = document.createElement('td'); 38 | } 39 | 40 | TableRow.prototype.setHost = function(host) { 41 | this.host = host; 42 | }; 43 | 44 | // DOM STUFF 45 | 46 | TableRow.prototype.appendChildren = function() { 47 | this.table.appendChild(this.row); 48 | this.row.appendChild(this.nameTD); 49 | this.row.appendChild(this.buttonTD); 50 | // this.row.appendChild(this.appIdTD); 51 | // this.row.appendChild(this.disconnectTD); 52 | }; 53 | 54 | // TableRow.prototype.setAppId = function() { 55 | // setElementText(this.appIdTD, this.application.app_id); 56 | // setElementDisplay(this.appIdTD, "none"); 57 | // } 58 | 59 | TableRow.prototype.createConnectButton = function() { 60 | const buttonTD = this.buttonTD; 61 | const domainBtn = document.createElement("button"); 62 | 63 | domainBtn.setAttribute("class", `${CONTRAST_BUTTON_CLASS} domainBtn`); 64 | buttonTD.appendChild(domainBtn); 65 | 66 | setElementText(domainBtn, CONNECT_BUTTON_TEXT); 67 | setElementText(this.nameTD, this.application.name.titleize()); 68 | 69 | domainBtn.addEventListener("click", () => { 70 | const cd = new ConnectedDomain(this.host, this.application); 71 | cd.connectDomain() 72 | .then(connected => this._showMessage(connected, true)) 73 | .catch(error => this._handleConnectError(error)); 74 | }); 75 | }; 76 | 77 | // REMOVE DOMAIN 78 | TableRow.prototype.renderDisconnect = function(storedApps, storedApp) { 79 | // const domain = connected._getDomainFromApplication(); 80 | const disconnectButton = document.createElement("button"); 81 | const connected = new ConnectedDomain(this.host, storedApp); 82 | 83 | const appHostSpan = document.createElement("span"); 84 | appHostSpan.innerText = Application.subDomainColonForUnderscore(this.host); 85 | appHostSpan.setAttribute("class", HOST_SPAN_CLASS); 86 | this.nameTD.appendChild(appHostSpan); 87 | 88 | setElementText(disconnectButton, DISCONNECT_BUTTON_TEXT); 89 | 90 | disconnectButton.setAttribute("class", CONTRAST_BUTTON_DISCONNECT_CLASS); 91 | 92 | disconnectButton.addEventListener("click", () => { 93 | connected 94 | .disconnectDomain(this) 95 | .then(disconnected => { 96 | if (disconnected) { 97 | this.removeDomainAndButton(); 98 | } else { 99 | throw new Error("Error Disconnecting Domain"); 100 | } 101 | }) 102 | .catch(error => this._handleConnectError(error)); 103 | }); 104 | this.buttonTD.appendChild(disconnectButton); 105 | }; 106 | 107 | TableRow.prototype.removeDomainAndButton = function() { 108 | this.buttonTD.innerHTML = ""; 109 | this.nameTD.innerHTML = encodeURIComponent(this.application.name); 110 | }; 111 | 112 | // HELPERS 113 | 114 | TableRow.prototype._showMessage = function(result, connect) { 115 | const message = document.getElementById("connected-domain-message"); 116 | const tableContainer = document.getElementById("table-container"); 117 | changeElementVisibility(message); 118 | if (result && connect) { 119 | this._successConnect(message); 120 | message.setAttribute("style", `color: ${CONTRAST_GREEN}`); 121 | setElementDisplay(tableContainer, "none"); 122 | // hideElementAfterTimeout(message); 123 | } else if (!result && connect) { 124 | this._failConnect(message); 125 | message.setAttribute("style", `color: ${CONTRAST_GREEN}`); 126 | setElementDisplay(tableContainer, "none"); 127 | // hideElementAfterTimeout(message); 128 | } else if (!result && !connect) { 129 | this._failDisconnect(message); 130 | message.setAttribute("style", `color: ${CONTRAST_RED}`); 131 | setElementDisplay(tableContainer, "none"); 132 | // hideElementAfterTimeout(message); 133 | } else { 134 | changeElementVisibility(message); 135 | setElementDisplay(tableContainer, "none"); 136 | } 137 | }; 138 | 139 | TableRow.prototype._handleConnectError = function(error) { 140 | const errorElement = document.getElementById('error-message-footer'); 141 | setElementDisplay(errorElement, "block"); 142 | setElementText(errorElement, `${error.toString()}`); 143 | setTimeout(() => setElementDisplay(errorElement, "none"), 10000); 144 | // const message = document.getElementById("connected-domain-message"); 145 | // this._failConnect(message); 146 | // hideElementAfterTimeout(message); 147 | // throw new Error(error); 148 | }; 149 | 150 | TableRow.prototype._successConnect = function(message) { 151 | setElementText(message, CONNECT_SUCCESS_MESSAGE); 152 | message.setAttribute("style", `color: ${CONTRAST_GREEN}`); 153 | chrome.runtime.sendMessage({ 154 | action: APPLICATION_CONNECTED, 155 | data: { 156 | domains: this._addHTTProtocol(this.host) 157 | } 158 | }); 159 | }; 160 | 161 | TableRow.prototype._failConnect = function(message) { 162 | setElementText(message, CONNECT_FAILURE_MESSAGE); 163 | message.setAttribute("style", `color: ${CONTRAST_RED}`); 164 | }; 165 | 166 | TableRow.prototype._failDisconnect = function(message) { 167 | setElementText(message, DISCONNECT_FAILURE_MESSAGE); 168 | message.setAttribute("style", `color: ${CONTRAST_RED}`); 169 | }; 170 | 171 | TableRow.prototype._addHTTProtocol = function(host) { 172 | host = Application.subDomainColonForUnderscore(host); 173 | let http = host; 174 | let https = host; 175 | if (!http.includes("http://")) { 176 | http = "http://" + host + "/*"; 177 | } 178 | if (!https.includes("https://")) { 179 | https = "https://" + host + "/*"; 180 | } 181 | return [http, https]; // eslint-disable-line 182 | }; 183 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _regenerator = require("babel-runtime/regenerator"); 8 | 9 | var _regenerator2 = _interopRequireDefault(_regenerator); 10 | 11 | var _asyncToGenerator2 = require("babel-runtime/helpers/asyncToGenerator"); 12 | 13 | var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); 14 | 15 | exports.indexFunction = indexFunction; 16 | 17 | var _util = require("./util.js"); 18 | 19 | var _Application = require("./models/Application.js"); 20 | 21 | var _Application2 = _interopRequireDefault(_Application); 22 | 23 | var _ApplicationTable = require("./models/ApplicationTable.js"); 24 | 25 | var _ApplicationTable2 = _interopRequireDefault(_ApplicationTable); 26 | 27 | var _Config = require("./models/Config.js"); 28 | 29 | var _Config2 = _interopRequireDefault(_Config); 30 | 31 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 32 | 33 | /** 34 | * indexFunction - Main function that's run, renders config button if user is on TS Organization Settings > API Page, otherwise renders vulnerability feed 35 | * 36 | * @return {void} 37 | */ 38 | /*eslint no-console: ["error", { allow: ["warn", "error", "log"] }] */ 39 | /*global 40 | chrome, 41 | document, 42 | URL, 43 | */ 44 | 45 | // import { 46 | // renderVulnerableLibraries, 47 | // } from './libraries/showLibraries.js' 48 | 49 | // import Application from './models/Application.js'; 50 | // import ApplicationLibrary from './libraries/ApplicationLibrary.js'; 51 | 52 | function indexFunction() { 53 | var _this = this; 54 | 55 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 56 | var tab = tabs[0]; 57 | var url = new URL(tab.url); 58 | (0, _util.getStoredCredentials)().then(function () { 59 | var _ref = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee(credentials) { 60 | var credentialed, application, config, table, _table; 61 | 62 | return _regenerator2.default.wrap(function _callee$(_context) { 63 | while (1) { 64 | switch (_context.prev = _context.next) { 65 | case 0: 66 | credentialed = (0, _util.isCredentialed)(credentials); 67 | _context.next = 3; 68 | return _Application2.default.retrieveApplicationFromStorage(tab); 69 | 70 | case 3: 71 | application = _context.sent; 72 | 73 | // if (!application) return; 74 | 75 | config = new _Config2.default(tab, url, credentialed, credentials, !!application); 76 | 77 | config.addListenerToConfigButton(); 78 | config.popupScreen(); 79 | if (!credentialed) { 80 | console.log("Please Configure the Extension"); 81 | } else if (credentialed && config._isContrastPage()) { 82 | table = new _ApplicationTable2.default(url); 83 | 84 | table.renderApplicationsMenu(); 85 | config.setGearIcon(); 86 | config.renderContrastUsername(); 87 | } else { 88 | config.setGearIcon(); 89 | config.renderContrastUsername(); 90 | if (!config._isContrastPage()) { 91 | _table = new _ApplicationTable2.default(url); 92 | 93 | _table.renderActivityFeed(); 94 | if (config.hasApp) { 95 | _table.renderApplicationsMenu(); 96 | } 97 | } 98 | } 99 | 100 | case 8: 101 | case "end": 102 | return _context.stop(); 103 | } 104 | } 105 | }, _callee, _this); 106 | })); 107 | 108 | return function (_x) { 109 | return _ref.apply(this, arguments); 110 | }; 111 | }()).catch(function (error) { 112 | return new Error(error); 113 | }); 114 | }); 115 | } 116 | 117 | /** 118 | * Run when popup loads 119 | */ 120 | document.addEventListener("DOMContentLoaded", indexFunction, false); 121 | // document.addEventListener('DOMContentLoaded', showLibrariesButton, false); 122 | 123 | // NOTE: SAVE THE BELOW FUNCTIONS FOR LIBRARY VULNS INTEGRATION 124 | // function showLibrariesButton() { 125 | // const refreshLibsButton = document.getElementById('scan-libs-text'); 126 | // const loadingElement = document.getElementById('libs-loading'); 127 | // 128 | // chrome.tabs.query({ active: true, currentWindow: true }, async(tabs) => { 129 | // if (!tabs || tabs.length === 0) return; 130 | // const tab = tabs[0]; 131 | // const app = await Application.retrieveApplicationFromStorage(tab); 132 | // if (app) { 133 | // refreshLibsButton.classList.remove('hidden'); 134 | // refreshLibsButton.classList.add('visible'); 135 | // 136 | // addListenerToRefreshButton(refreshLibsButton, loadingElement) 137 | // } 138 | // }); 139 | // } 140 | // 141 | // function addListenerToRefreshButton(refreshLibsButton, loadingElement) { 142 | // refreshLibsButton.addEventListener('click', function() { 143 | // _renderLoadingElement(loadingElement); 144 | // chrome.tabs.query({ active: true, currentWindow: true }, async(tabs) => { 145 | // if (!tabs || tabs.length === 0) return; 146 | // const tab = tabs[0]; 147 | // const app = await Application.retrieveApplicationFromStorage(tab); 148 | // const appLib = new ApplicationLibrary(tab, app); 149 | // try { 150 | // const libs = await appLib.getApplicationLibraries(); 151 | // if (!libs || libs.length === 0) { 152 | // _renderFoundVulnerableLibraries("No libraries with vulnerabilities found."); 153 | // _hideLoadingElement(loadingElement) 154 | // return; 155 | // } 156 | // const addedLibs = await appLib.addNewApplicationLibraries(libs); 157 | // if (addedLibs && addedLibs.length > 0) { 158 | // renderVulnerableLibraries(tab, app); 159 | // _renderFoundVulnerableLibraries(`Found ${addedLibs.length} libraries with vulnerabilities.`); 160 | // _hideLoadingElement(loadingElement); 161 | // } else { 162 | // _renderFoundVulnerableLibraries("No libraries with vulnerabilities found."); 163 | // _hideLoadingElement(loadingElement); 164 | // } 165 | // } catch (e) { 166 | // _renderFoundVulnerableLibraries("Error collecting libraries."); 167 | // _hideLoadingElement(loadingElement); 168 | // } 169 | // }); 170 | // }); 171 | // } 172 | // 173 | // function _renderFoundVulnerableLibraries(message) { 174 | // const libMessage = document.getElementById('found-libs-message'); 175 | // libMessage.innerText = message; 176 | // libMessage.classList.add('visible'); 177 | // libMessage.classList.remove('hidden'); 178 | // 179 | // setTimeout(() => { 180 | // libMessage.innerText = ''; 181 | // libMessage.classList.remove('visible'); 182 | // libMessage.classList.add('hidden'); 183 | // }, 3000); 184 | // } 185 | // 186 | // function _hideLoadingElement(loadingElement) { 187 | // loadingElement.style.visibility = 'hidden'; 188 | // // setElementDisplay(loadingElement, 'none'); 189 | // } 190 | // 191 | // function _renderLoadingElement(loadingElement) { 192 | // loadingElement.style.visibility = 'visible'; 193 | // // setElementDisplay(loadingElement, 'inline'); 194 | // } --------------------------------------------------------------------------------