Browse Source

Initial Public Commit

Ryan Howell 1 year ago
commit
a2dcfb6a66
100 changed files with 4572 additions and 0 deletions
  1. 51 0
      README.md
  2. 22 0
      heimdall-cli/.gitignore
  3. 41 0
      heimdall-cli/README.md
  4. 6 0
      heimdall-cli/benchmark.sh
  5. 219 0
      heimdall-cli/heimdall.js
  6. 23 0
      heimdall-cli/package.json
  7. 35 0
      heimdall-ext/.gitignore
  8. 35 0
      heimdall-ext/README.md
  9. 496 0
      heimdall-ext/background.js
  10. 6 0
      heimdall-ext/bootstrap/css/bootstrap.min.css
  11. 6 0
      heimdall-ext/bootstrap/js/bootstrap.bundle.min.js
  12. 18 0
      heimdall-ext/dist.js
  13. 20 0
      heimdall-ext/dist.sh
  14. BIN
      heimdall-ext/icons/A+_128.png
  15. BIN
      heimdall-ext/icons/A+_16.png
  16. BIN
      heimdall-ext/icons/A+_32.png
  17. BIN
      heimdall-ext/icons/A+_64.png
  18. BIN
      heimdall-ext/icons/A_128.png
  19. BIN
      heimdall-ext/icons/A_16.png
  20. BIN
      heimdall-ext/icons/A_32.png
  21. BIN
      heimdall-ext/icons/A_64.png
  22. BIN
      heimdall-ext/icons/B_128.png
  23. BIN
      heimdall-ext/icons/B_16.png
  24. BIN
      heimdall-ext/icons/B_32.png
  25. BIN
      heimdall-ext/icons/B_64.png
  26. BIN
      heimdall-ext/icons/C_128.png
  27. BIN
      heimdall-ext/icons/C_16.png
  28. BIN
      heimdall-ext/icons/C_32.png
  29. BIN
      heimdall-ext/icons/C_64.png
  30. BIN
      heimdall-ext/icons/D_128.png
  31. BIN
      heimdall-ext/icons/D_16.png
  32. BIN
      heimdall-ext/icons/D_32.png
  33. BIN
      heimdall-ext/icons/D_64.png
  34. BIN
      heimdall-ext/icons/F_128.png
  35. BIN
      heimdall-ext/icons/F_16.png
  36. BIN
      heimdall-ext/icons/F_32.png
  37. BIN
      heimdall-ext/icons/F_64.png
  38. BIN
      heimdall-ext/icons/H_128_green.png
  39. BIN
      heimdall-ext/icons/H_16_green.png
  40. BIN
      heimdall-ext/icons/H_32_green.png
  41. BIN
      heimdall-ext/icons/H_64_green.png
  42. BIN
      heimdall-ext/icons/U_128.png
  43. BIN
      heimdall-ext/icons/U_16.png
  44. BIN
      heimdall-ext/icons/U_32.png
  45. BIN
      heimdall-ext/icons/U_64.png
  46. 9 0
      heimdall-ext/libs/browser-polyfill/browser-polyfill.min.js
  47. 1 0
      heimdall-ext/libs/browser-polyfill/browser-polyfill.min.js.map
  48. 3 0
      heimdall-ext/libs/socket.io/socket.io.js
  49. 41 0
      heimdall-ext/manifest.json
  50. 46 0
      heimdall-ext/options.html
  51. 162 0
      heimdall-ext/options.js
  52. 24 0
      heimdall-ext/package.json
  53. 22 0
      heimdall-ext/popup.html
  54. 193 0
      heimdall-ext/popup.js
  55. 53 0
      heimdall-ext/report.html
  56. 191 0
      heimdall-ext/report.js
  57. 175 0
      heimdall-ext/style.css
  58. 20 0
      heimdall-library/.gitignore
  59. 22 0
      heimdall-library/README.md
  60. 260 0
      heimdall-library/index.js
  61. 84 0
      heimdall-library/lib/docker.js
  62. 80 0
      heimdall-library/lib/local-dns.js
  63. 101 0
      heimdall-library/lib/local-ocsp.js
  64. 56 0
      heimdall-library/lib/openssl.js
  65. 29 0
      heimdall-library/modules.js
  66. 145 0
      heimdall-library/modules/active/dns.js
  67. 101 0
      heimdall-library/modules/active/http.js
  68. 270 0
      heimdall-library/modules/active/tlscert.js
  69. 180 0
      heimdall-library/modules/active/tlsciphers.js
  70. 78 0
      heimdall-library/modules/active/tlsocsp.js
  71. 140 0
      heimdall-library/modules/active/tlspfs.js
  72. 59 0
      heimdall-library/modules/aggressive/heartbleed.js
  73. 102 0
      heimdall-library/modules/aggressive/ports.js
  74. 114 0
      heimdall-library/modules/passive/csp.js
  75. 74 0
      heimdall-library/modules/passive/hsts.js
  76. 53 0
      heimdall-library/modules/passive/https.js
  77. 64 0
      heimdall-library/modules/passive/referrerpolicy.js
  78. 52 0
      heimdall-library/modules/passive/server.js
  79. 67 0
      heimdall-library/modules/passive/versionnumbers.js
  80. 57 0
      heimdall-library/modules/passive/xcontenttypeoptions.js
  81. 50 0
      heimdall-library/modules/passive/xframeoptions.js
  82. 62 0
      heimdall-library/modules/passive/xxssprotection.js
  83. 28 0
      heimdall-library/package.json
  84. 6 0
      heimdall-library/test-certs/.gitignore
  85. 60 0
      heimdall-library/test-certs/ca.conf
  86. 45 0
      heimdall-library/test-certs/cert.conf
  87. 2 0
      heimdall-library/test-certs/cleanup.sh
  88. 98 0
      heimdall-library/test-certs/gencerts.sh
  89. 26 0
      heimdall-library/test-docker/build.sh
  90. 6 0
      heimdall-library/test-docker/heimdall-heartbleed/build.sh
  91. 1 0
      heimdall-library/test-docker/heimdall-nginx-http/301/.gitignore
  92. 7 0
      heimdall-library/test-docker/heimdall-nginx-http/301/Dockerfile
  93. 12 0
      heimdall-library/test-docker/heimdall-nginx-http/301/build.sh
  94. 21 0
      heimdall-library/test-docker/heimdall-nginx-http/301/nginx.conf
  95. 1 0
      heimdall-library/test-docker/heimdall-nginx-http/302/.gitignore
  96. 7 0
      heimdall-library/test-docker/heimdall-nginx-http/302/Dockerfile
  97. 12 0
      heimdall-library/test-docker/heimdall-nginx-http/302/build.sh
  98. 21 0
      heimdall-library/test-docker/heimdall-nginx-http/302/nginx.conf
  99. 1 0
      heimdall-library/test-docker/heimdall-nginx-http/noredirect/.gitignore
  100. 0 0
      heimdall-library/test-docker/heimdall-nginx-http/noredirect/Dockerfile

+ 51 - 0
README.md

@@ -0,0 +1,51 @@
+# heimdall
+
+A software system to improve analysis and communication of website domain security.
+
+Heimdall shows the feasibility of improving existing browser security indicators, by expanding the factors used for security analysis. The outcome of this analysis is communicated through a browser extension icon, using a simple colour coded A - F scoring system.
+
+This was Ryan Howell's final year project at university.
+
+## Links
+[Heimdall Project Website](https://heimdall.rhowell.io)
+
+[GOGS](https://box.rhowell.io/gogs/ryan/heimdall/)
+
+[GitHub Mirror](https://github.com/TheRyanHowell/heimdall)
+
+
+## Screenshots
+-----
+
+### Browser Extension Icon
+![Browser Extension Icon](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/icon.png)
+
+-----
+
+### Quick Report
+![Quick Report](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/quick-report.png)
+
+-----
+
+### Full Report
+![Full Report](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/full-report.png)
+
+-----
+
+### Web Extension Settings
+![Web Extension Settings](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/web-extension-settings.png)
+
+-----
+
+### Tray Applet
+![Tray Applet](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/tray.png)
+
+-----
+
+### Tray Settings
+![Tray Settings](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/tray-settings.png)
+
+-----
+
+### Command Line Interface
+![Command Line Interface](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/cli.png)

+ 22 - 0
heimdall-cli/.gitignore

@@ -0,0 +1,22 @@
+node_modules
+.DS_Store
+Thumbs.db
+*.log
+docs
+docs/*
+/docs
+
+bin
+bin/*
+/bin
+
+yarn.lock
+package-lock.json
+
+/dist
+/temp
+
+# ignore everything in 'app' folder what had been generated from 'src' folder
+/app/app.js
+/app/background.js
+/app/**/*.map

+ 41 - 0
heimdall-cli/README.md

@@ -0,0 +1,41 @@
+# heimdall-cli
+
+A CLI application for Heimdall.
+
+This component is a CLI application that runs the Heimdall library based on the input given.
+
+## Screenshot
+![Command Line Interface](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/cli.png)
+
+## Quick start
+
+Make sure you have [Node.js](https://nodejs.org) and OpenSSL installed, then type the following commands:
+```bash
+npm install
+npm run-script build
+```
+
+The dist folder will then contain binaries you can run.
+
+You can also run the file directly with node:
+```bash
+$ ./heimdall.js -h
+
+  Usage: heimdall scan example1.com http://example2.com https://example3.com
+
+
+  Options:
+
+    -V, --version            output the version number
+    -m  --mode <mode>        Mode of scan (quick, full), defaults to quick.
+    -t  --type <type>        Type of scan (passive, active, aggressive), defaults to passive.
+    -h, --help               output usage information
+
+
+  Commands:
+
+    scan <url> [otherUrls...]
+```
+
+## Benchmarking
+Modify benchmark.sh with a relevant domain, then run the script, this will generate a CSV for 10 minutes with the time to complete in seconds.

+ 6 - 0
heimdall-cli/benchmark.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+end=$(date -ud "10 minutes" +%s)
+while [[ $(date -u +%s) -le $end ]]
+do
+    ts=$(date +%s%N) ; node heimdall.js scan -m full -t aggressive example.com ; tt=$((($(date +%s%N) - $ts)/1000000)) ; echo "$tt" >> benchmark.csv
+done

+ 219 - 0
heimdall-cli/heimdall.js

@@ -0,0 +1,219 @@
+#!/usr/bin/env node
+'use strict';
+
+const program = require('commander');
+const url = require('url');
+const http = require('http');
+const https = require('https');
+const heimdall = require('heimdall-library');
+const fs = require('fs');
+const os = require('os');
+const { spawn } = require('child_process');
+
+var types = {
+  'passive': 0,
+  'active': 1,
+  'aggressive': 2
+};
+var modes = {
+  'quick': true,
+  'full': false
+};
+
+var urlsToScan = [];
+var responses = [];
+var numberOfResponses = 0;
+
+// Possible openssl locations
+const searchOpenSSLs = {
+  darwin: ['/usr/local/opt/openssl/bin/openssl'],
+  linux: ['/usr/bin/openssl', '/usr/local/bin/openssl', '/bin/openssl'],
+  win32: ['C:\\OpenSSL-Win64\\bin\\openssl.exe', 'C:\\OpenSSL-Win32\\bin\\openssl.exe']
+};
+
+// Attempts to find an openssl binary, fallsback to binary in path
+function findOpenSSL() {
+    var searchOpenSSL = searchOpenSSLs[process.platform];
+    for(var attempt of searchOpenSSL) {
+      if(fs.existsSync(attempt)) {
+        return attempt;
+      }
+    }
+
+    // Try path
+    return 'openssl';
+}
+
+// Openssl handler to be passed to Heimdall
+function openssl(parameters, callback, input) {
+  // By default generate a weak dhkey to test
+  if(!parameters) {
+    parameters = ['dhparam', '-dsaparam', '256'];
+  }
+
+  var openssl = spawn(findOpenSSL(), parameters);
+  var rData = '';
+
+  openssl.stdout.on('data', data => {
+      rData += data.toString('utf8');
+  });
+
+  openssl.on('error', function(err) {});
+
+  openssl.on('close', function(code) {
+    if(code === 0 && rData) {
+      callback(true, rData);
+    } else {
+      callback(false, null);
+    }
+  });
+
+  if(input) {
+    openssl.stdin.write(input);
+    openssl.stdin.end();
+  }
+}
+
+// Define CLI program
+program.version('1.0.0')
+       .usage('scan example1.com http://example2.com https://example3.com')
+       .option('-m  --mode <mode>', 'Mode of scan (quick, full), defaults to quick.')
+       .option('-t  --type <type>', 'Type of scan (passive, active, aggressive), defaults to passive.')
+       .command('scan <url> [otherUrls...]')
+       .action(function (url, otherUrls) {
+          urlsToScan.push(url);
+          if (otherUrls) {
+            otherUrls.forEach(function (oUrl) {
+              urlsToScan.push(oUrl);
+            });
+          }
+        });
+
+// Parse parameters
+program.parse(process.argv);
+
+// Send a request to the address, fallback to http on failure
+function sendRequest(oURL, callback) {
+  const options = {
+    hostname: oURL.hostname,
+    protocol: oURL.protocol,
+    timeout: 3000,
+    path: '/',
+    headers: {
+      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
+    },
+    rejectUnauthorized: false
+  };
+
+  var req = null;
+  if(oURL.protocol === 'https:') {
+    req = https.request(options, (res) => {
+      req.abort();
+      callback(true, res, oURL);
+    });
+  } else {
+    req = http.request(options, (res) => {
+      req.abort();
+      callback(true, res, oURL);
+    });
+  }
+
+
+  req.on('error', (e) => {
+    if(oURL.assumed) {
+      sendRequest(oURL.httpURL, callback);
+    } else {
+      callback(false, e, oURL);
+    }
+  });
+
+  req.end();
+}
+
+// Collect heimdall responses, on final response echo json output
+function handleHeimdallResponse(response) {
+  responses.push(response);
+  numberOfResponses++;
+  if(numberOfResponses === urlsToScan.length) {
+    console.log(JSON.stringify(responses, null, 1));
+    process.exit(0);
+  }
+}
+
+// Handle response from web server, by running heimdall
+function handleResponse(result, response, pURL) {
+  if(result) {
+    var request = {};
+    request.responseHeaders = [];
+    Object.keys(response.headers).forEach(function(key) {
+      request.responseHeaders.push({name: key, value: response.headers[key]});
+    });
+
+    request.url = pURL.href;
+    request.state = 'U';
+    request.quick = program.mode;
+    request.type = program.type;
+
+    heimdall.runModules(request, function(msg, response) {
+      handleHeimdallResponse(response);
+    }, openssl);
+
+  } else {
+    console.log('Error making request.');
+    console.log(response);
+    process.exit(1);
+  }
+}
+
+if(urlsToScan.length) {
+  // Create url objects from parameters
+  for(var sUrl of urlsToScan) {
+    var key = urlsToScan.indexOf(sUrl);
+    var newURL = url.parse(sUrl);
+    if(!newURL.protocol) {
+      let oldURL = newURL;
+      // Assume https
+      newURL = url.parse('https://' + oldURL.href);
+
+      // Fallback to http
+      newURL.assumed = true;
+      newURL.httpURL = url.parse('http://' + oldURL.href);
+    }
+
+    urlsToScan[key] = newURL;
+  }
+
+  if(!program.mode) {
+    program.mode = true;
+  } else if(!modes.hasOwnProperty(program.mode)) {
+    console.error('Invalid mode: ' + program.type);
+    process.exit(2);
+  } else {
+    program.mode = modes[program.mode];
+  }
+
+  if(!program.type) {
+    program.type = 'passive';
+  } else {
+    if(!types.hasOwnProperty(program.type)) {
+      console.error('Invalid type: ' + program.type);
+      process.exit(3);
+    }
+  }
+
+  // Test openssl works
+  openssl(false, function(result){
+    if(result) {
+      // Send requests to each url
+      for(var sUrl of urlsToScan) {
+        sendRequest(sUrl, handleResponse);
+      }
+    } else {
+      console.error('Unable to generate diffie hellman key, please install openssl.');
+      process.exit(4);
+    }
+  });
+
+} else {
+  program.help();
+}

+ 23 - 0
heimdall-cli/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "heimdall-cli",
+  "version": "0.0.8",
+  "description": "Heimdall command line interface",
+  "main": "index.js",
+  "scripts": {
+    "build": "./node_modules/pkg/lib-es5/bin.js heimdall.js --out-path ./bin/",
+    "test": "nyc -x test -x bin --reporter=text --reporter=lcov --check-coverage --lines 80 --per-file mocha --recursive --no-warnings"
+  },
+  "author": "",
+  "license": "GPL-3.0",
+  "dependencies": {
+    "commander": "^2.13.0",
+    "heimdall-library": "file:../heimdall-library",
+    "socket.io-client": "^2.0.4"
+  },
+  "devDependencies": {
+    "pkg": "^4.3.0",
+    "chai": "^4.1.2",
+    "mocha": "^5.0.4",
+    "nyc": "^11.4.1"
+  }
+}

+ 35 - 0
heimdall-ext/.gitignore

@@ -0,0 +1,35 @@
+node_modules
+.DS_Store
+Thumbs.db
+*.log
+docs
+docs/*
+web-ext-artifacts
+web-ext-artifacts/*
+
+.web-extension-id
+
+yarn.lock
+package-lock.json
+
+/dist
+/temp
+
+# ignore everything in 'app' folder what had been generated from 'src' folder
+/app/app.js
+/app/background.js
+/app/**/*.map
+
+.nyc_output
+coverage
+
+dist
+dist/*
+dist/**
+
+chromedist
+chromedist/*
+chromedist/**
+
+.api-key
+.issuer

+ 35 - 0
heimdall-ext/README.md

@@ -0,0 +1,35 @@
+# heimdall-ext
+
+A webextension for Heimdall.
+
+This component is a webextension that can run the passive modules of Heimdall, or connect to the Heimdall service in the background for the active modules.
+
+## Screenshots
+-----
+
+### Browser Extension Icon
+![Browser Extension Icon](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/icon.png)
+
+-----
+
+### Quick Report
+![Quick Report](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/quick-report.png)
+
+-----
+
+### Full Report
+![Full Report](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/full-report.png)
+
+-----
+
+### Web Extension Settings
+![Web Extension Settings](https://box.rhowell.io/gogs/ryan/heimdall/raw/master/screenshots/web-extension-settings.png)
+
+## Quick start
+
+Make sure you have [Node.js](https://nodejs.org) installed, then type the following commands:
+```bash
+npm install
+npm run-script build && ./dist.sh
+```
+This will build all files needed, from this point you can load the web-extension into the web browser.

+ 496 - 0
heimdall-ext/background.js

@@ -0,0 +1,496 @@
+'use strict';
+
+const heimdall = require('heimdall-library');
+const urlParse = require('url');
+
+// Setup cache and valid icon states
+var tabs = {};
+const validStates = [
+  'U',
+  'F',
+  'D',
+  'C',
+  'B',
+  'A',
+  'A+'
+];
+
+var types = {
+  0: 'passive',
+  1: 'active',
+  2: 'aggressive'
+};
+// Define options and their defaults
+const options = ['type', 'serviceAddress', 'servicePort', 'whitelist', 'blacklist'];
+const defaults = [0, '127.0.0.1', '3000', [''], ['']];
+
+// Init
+var socket = null;
+var onCompletedListener = null;
+var onBeforeRequestListener = null;
+var onMessageListener = null;
+var onConnectListener = null;
+var refreshInterval = null;
+
+var callbacks = {};
+var callbackIndex = 0;
+var ports = [];
+var usedHSTS = {};
+
+// Get a single extension setting
+function getSetting(key, onGot) {
+  browser.storage.local.get(key).then(function(result){
+    console.log(result);
+    if(result.hasOwnProperty(key)) {
+      onGot(result[key]);
+    } else {
+      onGot(defaults[options.indexOf(key)]);
+    }
+
+  }, function(e){
+    console.log(e);
+    onGot(defaults[options.indexOf(key)]);
+  });
+}
+
+// Get all the extension settings
+function getAllSettings(callback) {
+  var settings = {};
+  getSetting('type', function(type){
+    settings.type = type;
+    getSetting('serviceAddress', function(serviceAddress){
+      settings.serviceAddress = serviceAddress;
+      getSetting('servicePort', function(servicePort){
+        settings.servicePort = servicePort;
+        getSetting('whitelist', function(whitelist){
+          settings.whitelist = whitelist;
+          getSetting('blacklist', function(blacklist){
+            settings.blacklist = blacklist;
+            callback(settings);
+          });
+        });
+      });
+    });
+  });
+}
+
+// Updates the state for the tab relevant to the request
+function processStateChange(tabId) {
+  var icon = validStates[0];
+  // If relevant tab exists
+  if(tabId in tabs) {
+    if(validStates.includes(tabs[tabId].response.state)) {
+      // Set it's icon to the response state
+      icon = tabs[tabId].response.state;
+    }
+  }
+
+  // Attempt to change icon
+  console.log('Changing ' + tabId + ' to ' + icon);
+  browser.browserAction.setIcon({
+    path: {
+      "16": "./icons/" + icon + "_16.png",
+      "32": "./icons/" + icon + "_32.png",
+      "64": "./icons/" + icon + "_64.png",
+      "128": "./icons/" + icon + "_128.png"
+    },
+    tabId: parseInt(tabId)
+  }).catch(error => {
+    console.error(error);
+    console.log('DELETE 2');
+    // Clear from cache
+    delete tabs[tabId];
+  });
+}
+
+// Handle response from heimdall, triggering icon change when appropriate
+function handleHeimdallResponse(msg) {
+  console.log('Response: ');
+  console.log(msg);
+
+  if(msg.hasOwnProperty('response') && msg.response.hasOwnProperty('callbackIndex') && msg.response.callbackIndex in callbacks) {
+    console.log('CALLBACK');
+    var callback = callbacks[msg.response.callbackIndex];
+    delete callbacks[msg.response.callbackIndex];
+    delete msg.response.callbackIndex;
+    callback(msg);
+    return;
+  }
+
+  if(msg.tabID <= 0) {
+    return;
+  }
+
+  // If update
+  if(msg.tabId in tabs) {
+    // If older
+    if(msg.timeStamp < tabs[msg.tabId].timeStamp) {
+      console.log('Old Data');
+      return;
+    }
+  }
+
+  tabs[msg.tabId] = msg;
+  processStateChange(msg.tabId);
+}
+
+// Disconnect from tray applet
+function disconnect() {
+  if(socket) {
+    console.log('Disconnect')
+    socket.close();
+    setTimeout(function(){
+      console.log('Reconnect')
+      socket.open();
+    }, 5000);
+  } else {
+    console.log('Could not disconnect');
+  }
+}
+
+// Send request to heimdall, local or tray applet, after parsing for valid request
+function startHeimdall(response, quick, callback) {
+  var response = processHSTSUsed(response);
+  getAllSettings(function(settings) {
+    console.log(settings);
+
+    const pURL = urlParse.parse(response.url);
+
+    // Must match whitelist
+    var inWhiteList = false;
+    var hasWhiteList = false;
+    for(var white of settings.whitelist) {
+      console.log(white);
+      if(white && white.length) {
+        hasWhiteList = true;
+        try {
+          var regex = new RegExp(white, 'g');
+          if(regex.test(pURL.hostname)){
+            inWhiteList = true;
+            break;
+          }
+        } catch(e) {}
+      }
+    }
+
+    if(hasWhiteList && !inWhiteList) {
+      console.log('Hit whitelist');
+      return;
+    }
+
+    // Must not match blacklist
+    for(var black of settings.blacklist) {
+      console.log(black);
+      if(black && black.length) {
+        try {
+          var regex = new RegExp(black, 'g');
+          if(regex.test(pURL.hostname)){
+            console.log('Hit blacklist');
+            return;
+          }
+        } catch(e) {}
+      }
+    }
+
+    // Define how heimdall should scan
+    response.state = 'U';
+    response.quick = quick;
+    response.type = types[settings.type];
+    response.info = true;
+
+    // Send request to service if non passive, else run it ourselves
+    if(response.type !== types[0]) {
+      if(socket.connected) {
+        console.log('Sent Request');
+        console.log(response);
+        callbackIndex++;
+        response.callbackIndex = callbackIndex;
+        callbacks[callbackIndex] = callback;
+        socket.emit('request', heimdall.sanitizeRequest(response));
+      }
+    } else {
+      console.log('Locally Processed Request');
+      console.log(response);
+      heimdall.runModules(heimdall.sanitizeRequest(response), function(msg, response) {
+        callback({tabId: msg.tabId, response: response});
+      });
+    }
+  });
+}
+
+function validateResponse(response, cache) {
+  // Skip system tabs
+  if(response.tabID <= 0) {
+    console.log('Skipped 1');
+    return false;
+  }
+
+  console.log('VALIDATE');
+  console.log(cache);
+  console.log(response.tabId in tabs);
+  console.log(response.tabId);
+  console.log(tabs);
+
+  // If cache enabled we already have a result for that tab, and the url hasn't changed
+  if(cache && response.tabId in tabs) {
+    console.log('COMPARE');
+    console.log(response.url);
+    console.log(tabs[response.tabId].response.url);
+    if(response.url === tabs[response.tabId].response.url) {
+      console.log('Skipped 2');
+      return false;
+    } else {
+      // Invalidate cache
+      tabs[response.tabId].cacheinvalid = true;
+      tabs[response.tabId].response.state = 'U';
+      tabs[response.tabId].response.moduleResults = [];
+    }
+  }
+
+  const pURL = urlParse.parse(response.url);
+  console.log(pURL);
+
+  // Must be http or https
+  if(!['http:', 'https:'].includes(pURL.protocol)) {
+    console.log('Skipped 3');
+    return false;
+  }
+
+  // Must be port 80 or 443
+  if(pURL.port !== null && !['80', '443'].includes(pURL.port)) {
+    console.log('Skipped 4');
+    return false;
+  }
+
+  // Must not be localhost
+  if(['localhost', '127.0.0.1'].includes(pURL.hostname)) {
+    console.log('Skipped 5');
+    return false;
+  }
+
+  return true;
+}
+
+// When a request has completed on a main frame
+function onCompleted(response) {
+  response = stripURL(response);
+  if(validateResponse(response, true)) {
+    startHeimdall(response, true, handleHeimdallResponse);
+  }
+}
+
+// Strip sensitive information from url, we only scan base path
+function stripURL(object) {
+  if(object && object.hasOwnProperty('url')) {
+    const pURL = urlParse.parse(object.url);
+    object.url = pURL.protocol + '//' + pURL.hostname + '/';
+  }
+
+  return object;
+}
+
+// Handler used for onBeforeRequest event sent to web extension
+function onBeforeRequest(request) {
+  request = stripURL(request);
+  if(request.tabId !== -1) {
+    console.log(request.url);
+    console.log(tabs);
+    console.log(tabs[request.tabId]);
+    if(request.tabId in tabs && request.url !== tabs[request.tabId].response.url) {
+      console.log('DELETE 1');
+      delete tabs[request.tabId];
+      processStateChange(request.tabId);
+    }
+  }
+}
+
+// Clear the cache of each tabs responses
+function invalidateCache() {
+  for (var tab in tabs) {
+    if (tabs.hasOwnProperty(tab)) {
+      tabs[tab].cacheinvalid = true;
+      tabs[tab].response.state = 'U';
+      tabs[tab].response.moduleResults = [];
+      processStateChange(tab);
+    }
+  }
+}
+
+// Update all icons based on their cache data
+function updateAll() {
+  for (var tab in tabs) {
+    if (tabs.hasOwnProperty(tab)) {
+      if(validateResponse(tabs[tab].response, false)) {
+        startHeimdall(tabs[tab].response, true, handleHeimdallResponse);
+      }
+    }
+  }
+}
+
+// Handled incoming messages
+function handleMessage(request, sender, sendResponse) {
+  console.log(request);
+  console.log(sender);
+  console.log(sendResponse);
+
+  // Get quick from cache
+  if(request.hasOwnProperty('tabId') && request.tabId in tabs) {
+    console.log('Sending Quick Report');
+    return Promise.resolve({response: tabs[request.tabId]});
+  } else if(request.hasOwnProperty('restart') && request.restart) {
+    restart();
+    return Promise.resolve({restarted: true});
+  } else {
+    console.log('Sending Error State');
+    return Promise.resolve({error: true});
+  }
+}
+
+// Handle requests from report page
+function reportRequest(m) {
+  if(m.hasOwnProperty('request') && m.request && m.request.hasOwnProperty('bPortIndex') && validateResponse(m.request, false)) {
+    startHeimdall(m.request, false, function(response) {
+      ports[response.response.bPortIndex].postMessage({response: response});
+    });
+  }
+}
+
+// Handle page connection
+function pageConnected(p) {
+  var portIndex = ports.push(p) - 1;
+  // Bind to report requests
+  p.onMessage.addListener(reportRequest);
+  p.postMessage({index: portIndex});
+}
+
+// Handler for timer to refresh icons
+function refreshTimer() {
+  for (var tab in tabs) {
+    if (tabs.hasOwnProperty(tab) && !tabs[tab].hasOwnProperty('cacheinvalid')) {
+      processStateChange(tab);
+    }
+  }
+}
+
+// Shutdown handler, clean up objects/handlers
+function shutdown() {
+  if(socket) {
+    socket.destroy();
+    socket = null;
+  }
+
+  if(onCompletedListener) {
+    browser.webRequest.onCompleted.removeListener(onCompletedListener);
+  }
+
+  if(onBeforeRequestListener) {
+    browser.webRequest.onBeforeRequest.removeListener(onBeforeRequestListener);
+  }
+
+  if(onMessageListener) {
+    browser.webRequest.onMessage.removeListener(onMessageListener);
+  }
+
+  if(onConnectListener) {
+    browser.webRequest.onConnect.removeListener(onConnectListener);
+  }
+
+  if(refreshInterval) {
+    clearInterval(refreshInterval);
+  }
+
+  invalidateCache();
+}
+
+function restart() {
+  shutdown();
+  init();
+}
+
+// Workaround for chrome HSTS stripping
+function processHSTSUsed(response) {
+  if(response.hasOwnProperty('requestId') && usedHSTS.hasOwnProperty(response.requestId)) {
+    if(response.hasOwnProperty('responseHeaders') && response.responseHeaders) {
+      response.responseHeaders.push({name: 'X-Heimdall-HSTS', value: '1'});
+      delete usedHSTS[response.requestId];
+    }
+  }
+
+  return response;
+}
+
+// Handler for onBeforeRedirect chrome HSTS workaround
+function onBeforeRedirect(details) {
+  if(details.statusCode === 307 && details.responseHeaders && details.responseHeaders.length) {
+    for(var hIndex in details.responseHeaders) {
+      if(details.responseHeaders[hIndex].name === "Non-Authoritative-Reason" && details.responseHeaders[hIndex].value === "HSTS") {
+        usedHSTS[details.requestId] = true;
+        break;
+      }
+    }
+  }
+}
+
+// Entrypoint
+function init() {
+  // Get relevant settings
+  getSetting('serviceAddress', function(serviceAddress){
+    getSetting('servicePort', function(servicePort){
+      getSetting('type', function(type){
+        // Only connect in active mode
+        if(type === '1') {
+          // Connect to the server
+          socket = io('http://' + serviceAddress + ':' + servicePort);
+
+          socket.on('connect', function(){
+            console.log('Connected');
+          });
+
+          socket.on('connect_error', (error) => {
+            console.log(error);
+          });
+
+          socket.on('connect_timeout', (timeout) => {
+            console.log(timeout);
+          });
+
+          // When we get a response, update our cache and icon
+          socket.on('response', handleHeimdallResponse);
+          socket.on('heimdallDisconnect', disconnect);
+        }
+
+        // Bind to requests when completed on a main frame
+        onCompletedListener = browser.webRequest.onCompleted.addListener(
+          onCompleted,
+          {urls: ['<all_urls>'], types:['main_frame']},
+          ["responseHeaders"]
+        );
+
+        // Bind to onBeforeRequest for HSTS workaround
+        onBeforeRequestListener = browser.webRequest.onBeforeRequest.addListener(
+          onBeforeRequest,
+          {urls: ['<all_urls>'], types:['main_frame']}
+        );
+
+        // Bind to incoming messages
+        onMessageListener = browser.runtime.onMessage.addListener(handleMessage);
+
+        // Bind to report page
+        onConnectListener = browser.runtime.onConnect.addListener(pageConnected);
+
+        // Bind to incoming requests
+        browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect,
+          {urls: ['<all_urls>'], types:['main_frame']},
+          ["responseHeaders"]
+        );
+
+        // Set icon refresh timer
+        refreshInterval = setInterval(refreshTimer, 1000);
+
+        updateAll();
+      });
+    });
+  });
+}
+
+init();

File diff suppressed because it is too large
+ 6 - 0
heimdall-ext/bootstrap/css/bootstrap.min.css


File diff suppressed because it is too large
+ 6 - 0
heimdall-ext/bootstrap/js/bootstrap.bundle.min.js


+ 18 - 0
heimdall-ext/dist.js

@@ -0,0 +1,18 @@
+#!/usr/bin/env node
+'use strict';
+var fs = require('fs');
+
+// Load the app manifest
+var manifest = JSON.parse(fs.readFileSync('chromedist/manifest.json'));
+
+// Strip firefox specific values
+if(manifest.applications) {
+  delete manifest.applications;
+}
+
+if(manifest.options_ui && manifest.options_ui.browser_style) {
+  delete manifest.options_ui.browser_style;
+}
+
+// Write new manifest into chromedist folder
+fs.writeFileSync('chromedist/manifest.json', JSON.stringify(manifest));

+ 20 - 0
heimdall-ext/dist.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+mkdir -p chromedist
+cp manifest.json chromedist
+cp *.html chromedist
+cp *.css chromedist
+cp -R dist chromedist
+cp -R libs chromedist
+cp -R bootstrap chromedist
+cp -R icons chromedist
+
+mkdir -p firefoxdist
+cp manifest.json firefoxdist
+cp *.html firefoxdist
+cp *.css firefoxdist
+cp -R dist firefoxdist
+cp -R libs firefoxdist
+cp -R bootstrap firefoxdist
+cp -R icons firefoxdist
+
+node dist.js

BIN
heimdall-ext/icons/A+_128.png


BIN
heimdall-ext/icons/A+_16.png


BIN
heimdall-ext/icons/A+_32.png


BIN
heimdall-ext/icons/A+_64.png


BIN
heimdall-ext/icons/A_128.png


BIN
heimdall-ext/icons/A_16.png


BIN
heimdall-ext/icons/A_32.png


BIN
heimdall-ext/icons/A_64.png


BIN
heimdall-ext/icons/B_128.png


BIN
heimdall-ext/icons/B_16.png


BIN
heimdall-ext/icons/B_32.png


BIN
heimdall-ext/icons/B_64.png


BIN
heimdall-ext/icons/C_128.png


BIN
heimdall-ext/icons/C_16.png


BIN
heimdall-ext/icons/C_32.png


BIN
heimdall-ext/icons/C_64.png


BIN
heimdall-ext/icons/D_128.png


BIN
heimdall-ext/icons/D_16.png


BIN
heimdall-ext/icons/D_32.png


BIN
heimdall-ext/icons/D_64.png


BIN
heimdall-ext/icons/F_128.png


BIN
heimdall-ext/icons/F_16.png


BIN
heimdall-ext/icons/F_32.png


BIN
heimdall-ext/icons/F_64.png


BIN
heimdall-ext/icons/H_128_green.png


BIN
heimdall-ext/icons/H_16_green.png


BIN
heimdall-ext/icons/H_32_green.png


BIN
heimdall-ext/icons/H_64_green.png


BIN
heimdall-ext/icons/U_128.png


BIN
heimdall-ext/icons/U_16.png


BIN
heimdall-ext/icons/U_32.png


BIN
heimdall-ext/icons/U_64.png


File diff suppressed because it is too large
+ 9 - 0
heimdall-ext/libs/browser-polyfill/browser-polyfill.min.js


File diff suppressed because it is too large
+ 1 - 0
heimdall-ext/libs/browser-polyfill/browser-polyfill.min.js.map


File diff suppressed because it is too large
+ 3 - 0
heimdall-ext/libs/socket.io/socket.io.js


+ 41 - 0
heimdall-ext/manifest.json

@@ -0,0 +1,41 @@
+{
+  "manifest_version": 2,
+  "name": "Heimdall",
+  "version": "0.0.16",
+  "description": "A security scanner.",
+  "icons": {
+    "16": "icons/H_16_green.png",
+    "32": "icons/H_32_green.png",
+    "64": "icons/H_64_green.png",
+    "128": "icons/H_128_green.png"
+  },
+  "browser_action": {
+    "browser_style": true,
+    "default_popup": "popup.html",
+    "default_icon": {
+      "16": "icons/U_16.png",
+      "32": "icons/U_32.png",
+      "64": "icons/U_64.png",
+      "128": "icons/U_128.png"
+    }
+  },
+  "permissions": [
+    "webRequest",
+    "tabs",
+    "storage",
+    "<all_urls>"
+  ],
+  "background": {
+    "scripts": ["libs/browser-polyfill/browser-polyfill.min.js", "libs/socket.io/socket.io.js", "dist/background.compiled.js"]
+  },
+  "options_ui": {
+    "page": "options.html",
+    "browser_style": true
+  },
+  "applications": {
+    "gecko": {
+      "id": "heimdall@rhowell.io",
+      "strict_min_version": "58.0"
+    }
+  }
+}

+ 46 - 0
heimdall-ext/options.html

@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <div id="container">
+      <form>
+          <div class="browser-style">
+            <label for="type">Scan Type:</label>
+            <select id="type" class="browser-style">
+              <option value=0>Passive</option>
+              <option value=1>Active</option>
+            </select>
+          </div>
+          <div class="browser-style">
+            <label for="serviceAddress">Service Address:</label>
+            <input id="serviceAddress" type="text" value="127.0.0.1" class="browser-style" />
+          </div>
+
+          <div class="browser-style">
+            <label for="servicePort">Service Port:</label>
+            <input id="servicePort" type="text" value="3000" class="browser-style" />
+          </div>
+
+          <div class="browser-style">
+            <label for="whitelist">Whitelist (Regex):</label>
+            <div id="whitelist" class="browser-style">
+              <button id="addWhitelist" class="browser-style listButton" type="button">Add</button>
+            </div>
+          </div>
+
+          <div class="browser-style">
+            <label for="blacklist">Blacklist (Regex):</label>
+            <div id="blacklist" class="browser-style">
+              <button id="addBlacklist" class="browser-style listButton" type="button">Add</button>
+            </div>
+          </div>
+
+          <button type="submit" class="browser-style">Save</button>
+      </form>
+    </div>
+    <script src="libs/browser-polyfill/browser-polyfill.min.js"></script>
+    <script src="dist/options.compiled.js"></script>
+  </body>
+</html>

+ 162 - 0
heimdall-ext/options.js

@@ -0,0 +1,162 @@
+'use strict';
+
+// Define options and their defaults
+const options = ['type', 'serviceAddress', 'servicePort'];
+const defaults = [0, '127.0.0.1', '3000'];
+
+// Save the options when valid
+function saveOptions(e) {
+  e.preventDefault();
+
+  var data = {};
+  options.forEach(function(value) {
+    data[value] = document.querySelector("#" + value).value;
+  });
+
+  console.log(data);
+
+  // Validate port as integer
+  if(data.servicePort != parseInt(data.servicePort, 10)) {
+    alert('Failed to update settings, port is not an integer.');
+    return;
+  }
+
+  // Validate whitelist, blacklist
+  data['whitelist'] = getNewList('whitelist');
+  if(data['whitelist']) {
+    data['blacklist'] = getNewList('blacklist');
+    if(data['blacklist']) {
+      // Store new settings
+      browser.storage.local.set(data);
+
+      // Restart background thread
+      var sending = browser.runtime.sendMessage({restart: true});
+      sending.then(function(m) {
+        if(m && m.hasOwnProperty('restarted') && m.restarted) {
+          alert('Updated settings.');
+        } else {
+          alert('Failed to update settings.');
+        }
+      }, function(e) {
+        console.log(e);
+        alert('Failed to update settings.');
+      });
+    }
+  }
+}
+
+// Get the new list from html
+function getNewList(type) {
+  var list = document.getElementsByClassName(type + '-elem');
+  var returnItems = [];
+  for (var i = 0; i < list.length; i++) {
+    if(list[i].value && list[i].value.length) {
+      try {
+          new RegExp(list[i].value);
+          returnItems.push(list[i].value);
+      } catch(e) {
+        alert('"' + list[i].value + '" is not a valid regular expression.' + "\n\n" + e);
+        return false;
+      }
+    }
+  }
+
+  if(!returnItems.length) {
+    returnItems.push('');
+  }
+
+  return returnItems;
+}
+
+// Get the stored settings
+function getStoredList(type, callback) {
+  var getOption = browser.storage.local.get(type);
+  getOption.then(function (result) {
+    if(result.hasOwnProperty(type) && result[type]) {
+      callback(result[type]);
+    } else {
+      callback(['']);
+    }
+  }, function (error) {
+    callback(['']);
+  });
+}
+
+// Load stored settings into html
+function restoreOptions() {
+  options.forEach(function(value, index) {
+    var getOption = browser.storage.local.get(value);
+    getOption.then(function (result) {
+      document.querySelector("#" + value).value = result[value] || defaults[index];
+    }, function (error) {
+      console.log(error);
+    });
+  });
+
+  // Render stored whitelist
+  getStoredList('whitelist', function(result) {
+    console.log(result);
+    var whitelist = document.getElementById("whitelist");
+    var before = document.getElementById("addWhitelist");
+    result.forEach(function(value){
+      console.log(value);
+      var input = document.createElement('input');
+      input.setAttribute('class', 'whitelist-elem browser-style');
+      input.setAttribute('type', 'text');
+      input.setAttribute('placeholder', '(\.gov)(\.[a-z]{2})?$');
+      input.setAttribute('value', value);
+      whitelist.insertBefore(document.createElement('br'), before);
+      whitelist.insertBefore(input, before);
+    });
+  });
+
+  // Render stored blacklist
+  getStoredList('blacklist', function(result) {
+    console.log(result);
+    var blacklist = document.getElementById("blacklist");
+    var before = document.getElementById("addBlacklist");
+    result.forEach(function(value){
+      console.log(value);
+      var input = document.createElement('input');
+      input.setAttribute('class', 'blacklist-elem browser-style');
+      input.setAttribute('type', 'text');
+      input.setAttribute('placeholder', '(\.gov)(\.[a-z]{2})?$');
+      input.setAttribute('value', value);
+      blacklist.insertBefore(document.createElement('br'), before);
+      blacklist.insertBefore(input, before);
+    });
+  });
+}
+
+// Add a new item, bound to click of Add button
+function addListItem(type, before) {
+  var list = document.getElementById(type);
+  var input = document.createElement('input');
+  var before = document.getElementById(before);
+  input.setAttribute('class', type + '-elem browser-style');
+  input.setAttribute('type', 'text');
+  input.setAttribute('placeholder', '(\.gov)(\.[a-z]{2})?$');
+  input.setAttribute('value', '');
+  list.insertBefore(document.createElement('br'),before);
+  list.insertBefore(input,before);
+}
+
+// Bind to page loaded
+document.addEventListener('DOMContentLoaded', function() {
+  // Bind add button events
+  var addW = document.getElementById('addWhitelist');
+  addW.addEventListener('click', function() {
+      addListItem('whitelist', 'addWhitelist');
+  });
+
+  var addB = document.getElementById('addBlacklist');
+  addB.addEventListener('click', function() {
+      addListItem('blacklist', 'addBlacklist');
+  });
+
+  // Load current options
+  restoreOptions();
+
+  // Bind submit button event
+  document.querySelector("form").addEventListener("submit", saveOptions);
+});

+ 24 - 0
heimdall-ext/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "heimdall-ext",
+  "version": "0.0.16",
+  "description": "",
+  "scripts": {
+    "build": "browserify --exclude 'dns-packet' --exclude 'heartbleed-check-packet' --exclude 'ocsp' --exclude 'portscanner' background.js -g uglifyify -o dist/background.compiled.js && browserify -g uglifyify report.js -o dist/report.compiled.js && browserify -g uglifyify popup.js -o dist/popup.compiled.js && browserify -g uglifyify options.js -o dist/options.compiled.js",
+    "sign": "web-ext sign --ignore-files README.md dist.sh dist.js chromedist background.js popup.js report.js .api-key .issuer package.json package-lock.json heimdall-ext.pem --api-key `cat .issuer` --api-secret `cat .api-key`",
+    "test": "nyc -x test -x libs -x dist --reporter=text --reporter=lcov --check-coverage --lines 80 --per-file mocha --recursive --no-warnings"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "heimdall-library": "file:../heimdall-library",
+    "url": "^0.11.0"
+  },
+  "devDependencies": {
+    "browserify": "^16.1.0",
+    "uglifyify": "^4.0.5",
+    "web-ext": "^2.4.0",
+    "chai": "^4.1.2",
+    "mocha": "^5.0.4",
+    "nyc": "^11.4.1"
+  }
+}

+ 22 - 0
heimdall-ext/popup.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
+  <title>Heimdall</title>
+  <link rel="stylesheet" href="./bootstrap/css/bootstrap.min.css">
+  <script src="./bootstrap/js/bootstrap.min.js"></script>
+  <link href="./style.css" rel="stylesheet" type="text/css" />
+<body id="popupBody">
+  <div id="report"><a id="reportURL" href="#">View Full Report</a></div>
+  <div class="container-fluid">
+    <div class="row">
+        <div class="col-sm-12">
+          <div id="content"><p>Heimdall</p></div>
+        </div>
+    </div>
+  </div>
+  <script src="libs/browser-polyfill/browser-polyfill.min.js"></script>
+  <script type="text/javascript" src="./dist/popup.compiled.js"></script>
+</body>
+</html>

+ 193 - 0
heimdall-ext/popup.js

@@ -0,0 +1,193 @@
+'use strict';
+
+// Render a loading screen
+document.querySelector('#content').innerHTML = '<p>Loading</p>';
+
+const validStates = [
+  'U',
+  'F',
+  'D',
+  'C',
+  'B',
+  'A',
+  'A+'
+];
+
+function capitalizeFirstLetter(string) {
+    return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+// Render the popup
+function handleMessageResponse(m) {
+  document.querySelector('#content').innerHTML = '<p>Loading</p>';
+  console.log(m);
+  if(m.hasOwnProperty('response') && !m.hasOwnProperty('error')) {
+    if(m.response.hasOwnProperty('response')) {
+      // Header
+      var div = document.createElement('div');
+      var headerContainer = document.createElement('div');
+      var header = document.createElement('div');
+      var row = document.createElement('row');
+      var headerLeft = document.createElement('div');
+      var headerRight = document.createElement('div');
+      row.className = 'row';
+      row.className = 'row';
+      headerContainer.className = 'col-lg-8 offset-lg-2 col-md-8 offset-md-2 col-sm-8 offset-sm-2';
+      headerLeft.className = 'col-sm-6';
+      headerRight.className = 'col-sm-6';
+      header.setAttribute('id', 'headerSmall');
+
+      var overallRatingP = document.createElement('h3');
+      overallRatingP.innerText = 'Overall Rating';
+      header.appendChild(overallRatingP);
+
+      // Result
+      if(m.response.response.hasOwnProperty('state') && m.response.response.state && validStates.includes(m.response.response.state)) {
+        var overallRatingSmall = document.createElement('div');
+        overallRatingSmall.setAttribute('id', 'overallRatingSmall');
+
+        if(validStates.includes(m.response.response.state)) {
+          overallRatingSmall.innerText = m.response.response.state;
+          if(m.response.response.state === 'A+') {
+            overallRatingSmall.className = 'bgGradeAA';
+          } else {
+            overallRatingSmall.className = 'bgGrade' + m.response.response.state;
+          }
+
+        } else {
+          overallRatingSmall.innerText = 'U';
+          overallRatingSmall.className = 'bgGradeU';
+        }
+
+
+        headerLeft.appendChild(overallRatingSmall);
+      }
+
+      // Type
+      if(m.response.response.hasOwnProperty('type') && m.response.response.type) {
+        var p = document.createElement('p');
+        p.className = 'text-center text-sm-left';
+        p.innerText = 'Scan Type: ' + capitalizeFirstLetter(m.response.response.type);
+        headerRight.appendChild(p);
+      }
+
+      var p = document.createElement('p');
+      p.className = 'text-center text-sm-left';
+      p.innerText = 'Scan Mode: Quick';
+      headerRight.appendChild(p);
+
+
+      row.appendChild(headerLeft);
+      row.appendChild(headerRight);
+      header.appendChild(row);
+      headerContainer.appendChild(header);
+      div.appendChild(headerContainer);
+
+      // Module Result
+      if(m.response.response.state !== 'A+' && m.response.response.hasOwnProperty('moduleResults') && m.response.response.moduleResults && m.response.response.moduleResults.length) {
+        var tableContainer = document.createElement('div');
+        tableContainer.className = 'table-responsive';
+        var table = document.createElement('table');
+        table.className = 'table table-sm table-striped';
+        table.setAttribute('id', 'report-table');
+
+        var thead = document.createElement('thead');
+        var tr = document.createElement('tr');
+        var th1 = document.createElement('th');
+        var th2 = document.createElement('th');
+        tr.className = 'col1';
+        th1.innerText = 'Score';
+        th1.className = 'col2 text-center';
+        th2.innerText = 'Module';
+        th2.className = 'col3';
+        tr.appendChild(th1);
+        tr.appendChild(th2);
+        thead.appendChild(tr);
+        var tbody = document.createElement('tbody');
+        m.response.response.moduleResults.forEach(function(value) {
+          if(value.message) {
+            console.log('Adding: ' + value.message);
+            var tr = document.createElement('tr');
+            tr.className = '';
+            var td1 = document.createElement('td');
+            td1.className = 'col1 text-center';
+            var td2 = document.createElement('td');
+            td2.className = 'col2';
+
+            if(value.code) {
+              if(value.code === 6) {
+                td1.className += ' bgGradeAAHalf';
+              } else {
+                td1.className += ' bgGrade' + validStates[value.code] + 'Half';
+              }
+              td1.innerHTML = '<img class="smallkey" src="./icons/' + validStates[value.code] + '_128.png" width="128" height="128" title="' + validStates[value.code] + '">';
+            } else {
+              td1.className += ' bgGradeUHalf';
+              td1.innerHTML = '<img class="smallkey" src="./icons/U_128.png" width="128" height="128" title="U">';
+            }
+
+
+
+            if(value.link) {
+              var a1 = document.createElement('a');
+              var s1 = document.createElement('span');
+              s1.setAttribute('style', 'font-size: 80%;vertical-align: super;');
+              a1.setAttribute('href', value.link);
+              a1.setAttribute('target', '_blank');
+              a1.setAttribute('title', value.description);
+              a1.innerText = value.name;
+              a1.appendChild(s1);
+              td2.appendChild(a1);
+            } else {
+              td2.innerText = value.name;
+            }
+
+            tr.appendChild(td1);
+            tr.appendChild(td2);
+            tbody.appendChild(tr);
+          } else {
+            console.log('Skipped: ' + value.name);
+          }
+        })
+
+        table.appendChild(thead);
+        table.appendChild(tbody);
+        tableContainer.appendChild(table);
+        div.appendChild(tableContainer);
+      }
+
+      // Full Report URL
+      if(m.response.hasOwnProperty('tabId')) {
+        var reportURL = document.querySelector('#reportURL');
+        reportURL.target = '_blank';
+        reportURL.href = '/report.html?tabId=' + encodeURIComponent(m.response.tabId);
+        reportURL.setAttribute('style', 'display: block;');
+      }
+
+      document.querySelector('#content').innerHTML = '';
+      document.querySelector('#content').appendChild(div);
+    } else {
+      document.querySelector('#content').innerHTML = '<p>Error reading response from tray applet.</p>';
+    }
+  }
+}
+
+function handleMessageError(error) {
+  console.log(error);
+}
+
+// Handler for current tab, gets heimdall response data from background thread for relevant tab
+function currentTab(tabs) {
+  if(tabs && tabs.length) {
+    var sending = browser.runtime.sendMessage({tabId: tabs[0].id});
+    sending.then(handleMessageResponse, handleMessageError);
+  }
+}
+
+function onTabError(error) {
+  console.log(error);
+}
+
+// Get the currently active tab
+var querying = browser.tabs.query({active: true, currentWindow: true});
+querying.then(currentTab, onTabError);

+ 53 - 0
heimdall-ext/report.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
+  <title>Heimdall - Report</title>
+  <link href="./bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css">
+  <link href="./style.css" rel="stylesheet" type="text/css">
+  <link rel="shortcut icon" type="image/png" href="/icons/U_32.png" id="favicon"/>
+  <script src="./bootstrap/js/bootstrap.min.js" rel="stylesheet" type="text/css"></script>
+<body id="reportBody" style="max-width: 1200px;margin: 20px auto;">
+  <div class="container-fluid">
+    <div class="row">
+        <div class="col-lg-8 offset-lg-2 col-md-8 offset-md-2 col-sm-8 offset-sm-2">
+          <h1>Heimdall Report</h1>
+          <h2 id="title"></h2>
+          <div id="header">
+            <h3 class="text-center">Overall Rating</h3>
+            <div class="row">
+              <div class="col-sm-6">
+                <div id="overallRating"><span id="rating"></span></div>
+              </div>
+              <div class="col-sm-6" id="details">
+                <p class="text-center text-sm-left">Scan Type: <span id="scanType"></span></p>
+                <p class="text-center text-sm-left">Scan Mode: Full</p>
+              </div>
+            </div>
+          </div>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-sm-12">
+            <div id="content"><p>Full Report</p></div>
+        </div>
+    </div>
+
+    <div id="key">
+      <h4>Key:</h4>
+      <img class="key" title="A+" src="./icons/A+_128.png" width="128" height="128">
+      <img class="key" title="A" src="./icons/A_128.png" width="128" height="128">
+      <img class="key" title="B" src="./icons/B_128.png" width="128" height="128">
+      <img class="key" title="C" src="./icons/C_128.png" width="128" height="128">
+      <img class="key" title="D" src="./icons/D_128.png" width="128" height="128">
+      <img class="key" title="F" src="./icons/F_128.png" width="128" height="128">
+      <img class="key" title="U" src="./icons/U_128.png" width="128" height="128">
+      <p>Module scores (A+ to F) account for severity of assessed threat, where A+ is the best score possible.</p>
+    </div>
+  </div>
+  <script src="libs/browser-polyfill/browser-polyfill.min.js"></script>
+  <script src="./dist/report.compiled.js"></script>
+</body>
+</html>

+ 191 - 0
heimdall-ext/report.js

@@ -0,0 +1,191 @@
+'use strict';
+
+// Render loading screen
+document.querySelector('#content').innerHTML = '<p>Loading</p>';
+
+// Enable direct communication to background thread
+var bPort = browser.runtime.connect();
+var bPortIndex = null;
+
+const validStates = [
+  'U',
+  'F',
+  'D',
+  'C',
+  'B',
+  'A',
+  'A+'
+];
+
+function capitalizeFirstLetter(string) {
+    return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+// Render report
+function renderPage(m) {
+  document.querySelector('#content').innerHTML = '<p>Rendering report.</p>';
+  if(m.hasOwnProperty('response') && !m.hasOwnProperty('error')) {
+    // Header
+    var div = document.createElement('div');
+    var tableContainer = document.createElement('div');
+    tableContainer.className = 'table-responsive';
+    var table = document.createElement('table');
+    table.className = 'table table-sm table-striped';
+    table.setAttribute('id', 'report-table');
+
+    var thead = document.createElement('thead');
+    var tbody = document.createElement('tbody');
+
+    // Title
+    if(m.response.hasOwnProperty('url') && m.response.url) {
+      document.querySelector('#title').innerText = m.response.url;
+    }
+
+    // Result
+    if(m.response.hasOwnProperty('state') && m.response.state && validStates.includes(m.response.state)) {
+      var rating = document.querySelector('#rating');
+      var overallRating = document.querySelector('#overallRating');
+
+      if(validStates.includes(m.response.state)) {
+        rating.innerText = m.response.state;
+        if(m.response.state === 'A+') {
+          overallRating.className = 'bgGradeAA';
+        } else {
+          overallRating.className = 'bgGrade' + m.response.state;
+        }
+
+      } else {
+        rating.innerText = 'U';
+        overallRating.className = 'bgGradeU';
+      }
+    }
+
+    // Type
+    if(m.response.hasOwnProperty('type') && m.response.type) {
+      document.querySelector('#scanType').innerText = capitalizeFirstLetter(m.response.type);
+    }
+
+    // Module Result
+    if(m.response.hasOwnProperty('moduleResults') && m.response.moduleResults && m.response.moduleResults.length) {
+      var h4 = document.createElement('h4');
+      h4.innerText = 'Full Report';
+      div.appendChild(h4);
+
+      // Build table
+      var tr = document.createElement('tr');
+      var th1 = document.createElement('th');
+      var th2 = document.createElement('th');
+      var th3 = document.createElement('th');
+      tr.className = '';
+      th1.innerText = 'Score';
+      th1.className = 'col1 text-center';
+      th2.innerText = 'Module';
+      th2.className = 'col2';
+      th3.innerText = 'Message';
+      th3.className = 'col3';
+      tr.appendChild(th1);
+      tr.appendChild(th2);
+      tr.appendChild(th3);
+      thead.appendChild(tr);
+
+      // Loop through each result
+      m.response.moduleResults.forEach(function(value) {
+        // Create row
+        var tr = document.createElement('tr');
+        tr.className = '';
+
+        // Create columns
+        var td1 = document.createElement('td');
+        td1.className = 'col1 text-center';
+        var td2 = document.createElement('td');
+        td2.className = 'col2';
+        var td3 = document.createElement('td');
+        td3.className = 'col3 border-left';
+
+        // Score column
+        if(value.code === 6) {
+          td1.className += ' bgGradeAAHalf';
+        } else {
+          td1.className += ' bgGrade' + validStates[value.code] + 'Half';
+        }
+
+        if(validStates.length >= value.code) {
+          td1.innerHTML = '<img class="smallkey" src="./icons/' + validStates[value.code] + '_128.png" width="128" height="128" title="' + validStates[value.code] + '">';
+          document.querySelector('#favicon').setAttribute('href', '/icons/' + validStates[value.code] + '_32.png');
+        } else {
+          td1.innerHTML = '<img class="smallkey" src="./icons/U_128.png" width="128" height="128" title="U">';
+          document.querySelector('#favicon').setAttribute('href', '/icons/U_32.png');
+        }
+
+        // Module column
+        if(value.link) {
+          var a1 = document.createElement('a');
+          var s1 = document.createElement('span');
+          s1.setAttribute('style', 'font-size: 80%;vertical-align: super;');
+          a1.setAttribute('href', value.link);
+          a1.setAttribute('target', '_blank');
+          a1.setAttribute('title', value.description);
+          a1.innerText = value.name;
+          a1.appendChild(s1);
+          td2.appendChild(a1);
+        } else {
+          td2.innerText = value.name;
+        }
+
+        // Message column
+        if(value.message) {
+          td3.innerText = value.message;
+        }
+
+        // Build row and append to table body
+        tr.appendChild(td1);
+        tr.appendChild(td2);
+        tr.appendChild(td3);
+        tbody.appendChild(tr);
+      })
+
+      // Build final table
+      table.appendChild(thead);
+      table.appendChild(tbody);
+      tableContainer.appendChild(table);
+      div.appendChild(tableContainer);
+
+      // Render it
+      document.querySelector('#content').innerHTML = '';
+      document.querySelector('#header').setAttribute('style', 'display: block;');
+      document.querySelector('#content').appendChild(div);
+    }
+  }
+}
+
+
+function handleMessageError(error) {
+  document.querySelector('#content').innerHTML = '<p>Error sending message to background thread.</p>';
+}
+
+// Handler, sends request to background thread for full report
+function handleFirstMessageResponse(m) {
+  if(m.hasOwnProperty('response') && !m.hasOwnProperty('error') && m.response.hasOwnProperty('response')) {
+    document.querySelector('#content').innerHTML = '<p>Running full scan, please wait.</p>';
+    m.response.response.quick = false;
+    m.response.response.bPortIndex = bPortIndex;
+    bPort.postMessage({request: m.response.response});
+  } else {
+    document.querySelector('#content').innerHTML = '<p>Unable to get quick report.</p>';
+  }
+}
+
+// Handler for receiving a message from background thread port
+bPort.onMessage.addListener(function(m) {
+  if(m.hasOwnProperty('index')) {
+    // If we have an index, use it
+    bPortIndex = m.index;
+    var tabId = decodeURIComponent(new URL(window.location).searchParams.get('tabId'));
+
+    var sendingFirst = browser.runtime.sendMessage({tabId: tabId});
+    sendingFirst.then(handleFirstMessageResponse, handleMessageError);
+  } else if(m.hasOwnProperty('response')) {
+    // If we have a response, render it
+    renderPage(m.response);
+  }
+});

+ 175 - 0
heimdall-ext/style.css

@@ -0,0 +1,175 @@
+html, body {
+  background: #FFF;
+  color: #000;
+  width: 100%;
+}
+
+body {
+  white-space: nowrap;
+}
+
+#reportBody {
+  max-width: 1200px;
+}
+
+#popupBody {
+  padding: 10px 10px;
+}
+
+#reportURL {
+  position: absolute;
+  top: 10px;
+  right: 20px;
+  z-index: 9999;
+  display: none;
+}
+
+#key {
+  margin: 40px 0;
+}
+
+img.key {
+  width: auto;
+  height: 1.5em;
+  border-radius: 10%;
+}
+
+img.smallkey {
+  width: auto;
+  height: 1.5em;
+  border-radius: 10%;
+}
+
+#header {
+    display: none;
+    background-color: rgba(0,0,0,.05);
+    border-radius: 25px;
+    margin: 22px auto 15px;
+    padding: 10px;
+}
+
+#header h3 {
+  text-align: center;
+  margin-bottom: 20px;
+}
+
+#headerSmall {
+    background-color: rgba(0,0,0,.05);
+    border-radius: 18px;
+    margin: 22px auto 15px;
+    padding: 10px;
+}
+
+#headerSmall h3 {
+  text-align: center;
+  margin-bottom: 15px;
+}
+
+#headerSmall > div {
+  width: 150px;
+}
+
+
+#overallRating {
+  color: #FFF;
+  width: 128px;
+  font-size: 96px;
+  text-align: center;
+  line-height: 128px;
+  margin: 0 auto 10px auto;
+  border: 3px solid rgb(230,230,230);
+  border-radius: 10%;
+}
+
+#overallRatingSmall {
+  color: #FFF;
+  width: 96px;
+  font-size: 68px;
+  text-align: center;
+  line-height: 96px;
+  margin: 0 auto 8px auto;
+  border: 2px solid rgb(230,230,230);
+  border-radius: 8%;
+}
+
+.bgGradeU {
+  background-color: rgb(158,158,158);
+}
+
+.bgGradeF {
+  background-color: rgb(244,67,54);
+}
+
+
+.bgGradeD {
+  background-color: rgb(255,152,0);
+}
+
+.bgGradeC {
+  background-color: rgb(253,216,53);
+}
+
+.bgGradeB {
+  background-color: rgb(205,220,57);
+}
+
+.bgGradeA {
+  background-color: rgb(76,175,80);
+}
+
+.bgGradeAA {
+  background-color: rgb(33,150,243);
+}
+
+.bgGradeUHalf {
+  background-color: rgba(158,158,158,1);
+}
+
+.bgGradeFHalf {
+  background-color: rgba(244,67,54,1);
+}
+
+
+.bgGradeDHalf {
+  background-color: rgba(255,152,0,1);
+}
+
+.bgGradeCHalf {
+  background-color: rgba(253,216,53,1);
+}
+
+.bgGradeBHalf {
+  background-color: rgba(205,220,57,1);
+}
+
+.bgGradeAHalf {
+  background-color: rgba(76,175,80,1);
+}
+
+.bgGradeAAHalf {
+  background-color: rgba(33,150,243,1);
+}
+
+#report-table thead th {
+  border-bottom: none;
+}
+
+#report-table td {
+  padding: .3em .6em;
+}
+
+#report-table .border-left {
+  border-left: rgba(0,0,0,.05);
+}
+
+.col1 {
+  width: 1%;
+}
+
+.col2 {
+  width: 2%;
+}
+
+.col3 {
+  width: 97%;
+}

+ 20 - 0
heimdall-library/.gitignore

@@ -0,0 +1,20 @@
+node_modules
+.DS_Store
+Thumbs.db
+*.log
+docs
+docs/*
+
+yarn.lock
+package-lock.json
+
+/dist
+/temp
+
+# ignore everything in 'app' folder what had been generated from 'src' folder
+/app/app.js
+/app/background.js
+/app/**/*.map
+
+.nyc_output
+coverage

+ 22 - 0
heimdall-library/README.md

@@ -0,0 +1,22 @@
+# heimdall-library
+A library for Heimdall.
+
+This component is a library used by other components that exposes a set of modules and a runner that can be used to scan a domain response.
+
+## Quick start
+
+Make sure you have [Node.js](https://nodejs.org) and OpenSSL installed, then type the following commands:
+```bash
+npm install
+npm start
+```
+
+## Testing
+Before you run the test suite you must build it at least once:
+Generate certificates by running test-certs/gencerts.sh
+Build the docker containers by running test-docker/build.sh
+
+To run the test suite, type the following commands as root:
+```bash
+npm test
+```

+ 260 - 0
heimdall-library/index.js

@@ -0,0 +1,260 @@
+var exports = module.exports = {};
+
+var modules = require('./modules');
+const parseURL = require('url');
+
+var states = {
+  0: 'U',
+  1: 'F',
+  2: 'D',
+  3: 'C',
+  4: 'B',
+  5: 'A',
+  6: 'A+'
+};
+
+var types = {
+  'passive': 0,
+  'active': 1,
+  'aggressive': 2
+};
+
+// Process the final result, being the lowest of module results
+function processResult(values) {
+  var lowest = 6;
+  values.forEach(function(result) {
+    if(result.code < lowest) {
+      lowest = result.code;
+    }
+  });
+
+  return states[lowest];
+}
+
+// Order the result highest to lowest.
+function orderResult(values) {
+  values.sort(function(a, b) {
+      return a.code - b.code;
+  });
+
+  return values;
+}
+
+// Set headers to sanitize
+const disallowedHeaders = ['set-cookie', 'authorization'];
+
+// Delete openssl object from response
+function cleanResponse(object) {
+  if(object.hasOwnProperty('openssl')) {
+    delete object['openssl'];
+  }
+}
+
+// Sanitize request of sensitive information, determine if aggressive is allowed
+exports.sanitizeRequest = function(object) {
+  object = mapHeaders(object);
+
+  const oURL = parseURL.parse(object.url);
+  object.url = oURL.protocol + '//' + oURL.hostname + oURL.pathname;
+  object.allowagressive = false;
+
+  if(object.responseHeaders) {
+    // Remove disallowed headers
+    for(var disallowedHeader of disallowedHeaders) {
+      if(object.responseHeaders.hasOwnProperty(disallowedHeader)) {
+        delete object.responseHeaders[disallowedHeader];
+      }
+    }
+
+    if(object.responseHeaders.hasOwnProperty('x-heimdall-allow')) {
+      object.allowagressive = true;
+    }
+  }
+
+  return object;
+}
+
+// Map headers array to object
+function mapHeaders(object) {
+  if(object.hasOwnProperty('responseHeaders') && Array.isArray(object.responseHeaders)) {
+    var responseHeaders = {};
+    for(var header of object.responseHeaders) {
+      responseHeaders[header.name.toLowerCase()] = header.value;
+    }
+    object.responseHeaders = responseHeaders;
+  }
+
+  return object;
+}
+
+// Determine if the request contains a valid url and headers
+function isValidRequest(object) {
+  if(object && object.hasOwnProperty('url') && object.hasOwnProperty('responseHeaders')) {
+    const oURL = parseURL.parse(object.url);
+    if(oURL.protocol && oURL.hostname){
+      return true;
+    }
+  }
+
+  return false;
+}
+
+// Entry point for library use
+exports.runModules = async function(object, callback, openssl) {
+  var defaultResponse = object;
+  var response = defaultResponse;
+  if(!isValidRequest(object)) {
+    response.state = 'U';
+    cleanResponse(object);
+    callback(object, defaultResponse);
+    return;
+  }
+
+  if(openssl) {
+    object.openssl = openssl;
+  }
+
+  object = exports.sanitizeRequest(object);
+
+  object.state = 'U';
+
+  if(!object.hasOwnProperty('type')) {
+    object.type = 'passive';
+  }
+
+  // Determine if quick or full mode
+  if(object.hasOwnProperty('quick') && object.quick) {
+    // Process modules by severity (lowest state first)
+    var promises = [];
+    try {
+      Object.keys(modules).forEach(function(key) {
+        if(types[object.type] >= modules[key].type) {
+          if(object.allowagressive || modules[key].type !== 2) {
+            promises.push({lowestState: modules[key].lowestState, run: modules[key].run, name: modules[key].name, type: modules[key].type, depends: modules[key].depends, started: false, completed: false});
+          }
+        }
+      });
+
+      // Sort by state, then type
+      promises.sort(function(a, b) {
+        var lowestState = a.lowestState - b.lowestState;
+        if(lowestState === 0) {
+          return a.type - b.type
+        } else {
+          return lowestState;
+        }
+      });
+
+      var blockedPromises = {};
+      var completedPromises = {};
+      var completed = false;
+      var moduleResults = [];
+      var currentState = {code: 6};
+      var moduleNames = {};
+
+      for(promise of promises) {
+        moduleNames[promise.name] = true;
+      }
+
+      // Process modules in batches
+      do {
+        // Determine which modules to run this loop
+        for(promise of promises) {
+          if(!promise.started) {
+            var dependenciesCompleted = true;
+            if(promise.depends.length) {
+              for(depend of promise.depends) {
+                if(moduleNames.hasOwnProperty(depend)) {
+                  if(!completedPromises.hasOwnProperty(depend)) {
+                    dependenciesCompleted = false;
+                    break;
+                  }
+                }
+              }
+            }
+
+            if(dependenciesCompleted) {
+              if(blockedPromises.hasOwnProperty(promise.name)) {
+                delete blockedPromises[promise.name];
+              }
+
+              promise.promise = promise.run(object);
+              promise.started = true;
+            } else {
+
+              blockedPromises[promise.name] = true;
+            }
+          }
+        }
+
+        // Get the modules state
+        // Return when we know the lowest state possible has been reached
+        for(promise of promises) {
+          if(promise.lowestState < currentState.code) {
+            if(promise.started && !promise.completed) {
+              var newState = null;
+              try {
+                newState = await promise.promise;
+              } catch(e) {
+                newState = {code: 0, message: 'Unknown error occurred.'};
+              }
+
+              if(newState.code < currentState.code) {
+                currentState = newState;
+              }
+              moduleResults.push(newState);
+              promise.completed = true;
+              completedPromises[promise.name] = true;
+            }
+          } else {
+            // Unblock module if blocked, no longer relevant
+            if(blockedPromises.hasOwnProperty(promise.name)) {
+              delete blockedPromises[promise.name];
+            }
+            promise.started = true;
+            promise.completed = true;
+          }
+        }
+
+      } while(Object.keys(blockedPromises).length);
+
+      response.moduleResults = orderResult(moduleResults);
+      response.state = states[currentState.code];
+      cleanResponse(object);
+      callback(object, response);
+    } catch(e) {
+      cleanResponse(object);
+      callback(object, defaultResponse);
+    }
+  } else {
+    // Process all modules, full mode
+    var promises = [];
+
+    // Determine what modules to run, if aggressive is allowed etc.
+    try {
+      Object.keys(modules).forEach(function(key) {
+        if(types[object.type] >= modules[key].type) {
+          if(object.allowagressive || modules[key].type !== 2) {
+            promises.push(modules[key].run(object));
+          }
+        }
+      });
+
+      // Run all modules
+      Promise.all(promises).then(moduleResults => {
+        response.moduleResults = orderResult(moduleResults);
+        response.state = processResult(moduleResults);
+        cleanResponse(object);
+        callback(object, response);
+      }, reason => {
+        response.state = 'U';
+        cleanResponse(object);
+        callback(object, defaultResponse);
+      });
+
+    } catch(e) {
+      cleanResponse(object);
+      callback(object, defaultResponse);
+    }
+  }
+}

+ 84 - 0
heimdall-library/lib/docker.js

@@ -0,0 +1,84 @@
+#!/usr/bin/env node
+'use strict';
+var exports = module.exports = {};
+
+const fs = require('fs');
+const { spawn, exec } = require('child_process');
+
+// Function for calling docker command line
+exports.command = function(parameters, callback, input) {
+  var docker = spawn('docker', parameters);
+  var rData = '';
+
+  docker.stdout.on('data', data => {
+      rData += data.toString('utf8');
+  });
+
+  docker.on('error', function(err) {});
+
+  docker.on('close', function(code) {
+    setTimeout(function(){
+      if(code === 0 && rData) {
+        callback(true, rData);
+      } else {
+        callback(false, null);
+      }
+    }, 500);
+  });
+}
+
+// Check if a container is running
+function isRunning(callback) {
+  exec('docker ps | grep heimdall',
+    function(error, stdout, stderr) {
+      if (error === null) {
+        if(stdout && stdout.length) {
+          callback(true);
+        } else {
+          callback(false);
+        }
+
+      } else {
+        callback(false);
+      }
+  });
+}
+
+// Stops all running containers with heimdall in the name
+function stopHeimdallContainers(callback) {
+  exec('docker stop $(docker ps | grep heimdall | awk \'{print $1}\')',
+    function(error, stdout, stderr) {
+      if (error === null) {
+        callback(true);
+      } else {
+        callback(false);
+      }
+  });
+}
+
+// Stops all containers 
+function stopAll(callback) {
+  isRunning(function(running){
+    if(running) {
+      stopHeimdallContainers(function(stopped) {
+        if(stopped) {
+          isRunning(function(running){
+            if(running) {
+              setTimeout(function(){
+                stopAll(callback);
+              }, 500);
+            } else {
+              callback(true);
+            }
+          });
+        } else {
+          callback(false);
+        }
+      });
+    } else {
+      callback(true);
+    }
+  });
+};
+
+exports.stopAll = stopAll;

+ 80 - 0
heimdall-library/lib/local-dns.js

@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+'use strict';
+var exports = module.exports = {};
+
+const EventEmitter = require('events');
+const NetSocket = require('net').Socket;
+const packet = require('dns-packet');
+
+// Create dns event emitter for test suite
+class DnsEventEmitter extends EventEmitter {
+    constructor() {
+      super();
+      this.timeoutFunc = null;
+    }
+
+    connect(dnsPort, dnsServer, callback) {
+      callback();
+    };
+
+    write(request) {
+      var dData = packet.streamDecode(request);
+
+      let _this = this;
+      var res = {};
+
+      // Give correct response based on test case
+      if(dData.questions[0].type === 'CAA' && (dData.questions[0].name === 'caa.heimdall.local' || dData.questions[0].name === 'caads.heimdall.local')) {
+        res = {
+          answers: [{
+            type: 'CAA',
+            data: 'abc'
+          }]
+        };
+      } else if(dData.questions[0].type === 'DS' && (dData.questions[0].name === 'ds.heimdall.local' || dData.questions[0].name === 'caads.heimdall.local')) {
+        res = {
+          answers: [{
+            type: 'DS',
+            data: 'abc'
+          }]
+        };
+
+      } else if(dData.questions[0].name === 'timeout.heimdall.local') {
+        return;
+      }
+
+      _this.emit('data', res);
+    }
+
+    setTimeout(timeout) {
+      let _this = this;
+
+      if(this.timeoutFunc) {
+        clearTimeout(this.timeoutFunc);
+      }
+
+      this.timeoutFunc = setTimeout(function(){
+        _this.emit('timeout');
+        _this.emit('end');
+      }, timeout);
+    }
+
+    destroy() {
+      if(this.timeoutFunc) {
+        clearTimeout(this.timeoutFunc);
+      }
+    }
+
+    end() {
+      this.emit('end');
+    }
+}
+
+// Define how we handle dns if in test mode
+exports.Socket = function(local){
+  if(local) {
+    return new DnsEventEmitter();
+  } else {
+    return new NetSocket();
+  }
+};

+ 101 - 0
heimdall-library/lib/local-ocsp.js

@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+'use strict';
+var exports = module.exports = {};
+const tls = require('tls');
+const ocsp = require('ocsp');
+
+// Create OCSP handler for test suite
+// Define responses based on test case
+// tlsResult, hasOCSP, isValid, ocspErr
+function localTestOCSP(url, callback) {
+  switch(url) {
+    case 'ocsp-good.heimdall.local':
+      callback(true, true, true);
+    break;
+
+    case 'ocsp-bad.heimdall.local':
+      callback(true, true, false, 'Err');
+    break;
+
+    case 'ocsp-bad2.heimdall.local':
+      callback(true, true, false, false);
+    break;
+
+    default:
+      callback(false, false, false);
+    break;
+  }
+}
+
+// Define real OCSP for runtime
+function remoteTestOCSP(url, callback) {
+  var hasOCSP = false;
+
+  // Open a tls socket, requesting OCSP
+  const tlsSocket = tls.connect({
+    host: url,
+    servername: url,
+    port: 443,
+    handshakeTimeout: 5,
+    requestOCSP: true
+  });
+
+  tlsSocket.setTimeout(5000);
+  tlsSocket.on('timeout', () => {
+    tlsSocket.destroy();
+    callback(false, new Error('Timeout'));
+  });
+
+  tlsSocket.on('OCSPResponse', function(response) {
+    // Record if we got a OCSP response
+    if(response) {
+      hasOCSP = true;
+    }
+  });
+
+  tlsSocket.on('secureConnect', function() {
+    // If valid connection
+    if(tlsSocket.authorized) {
+      // And has OCSP
+      if(hasOCSP) {
+        // Check for valid OCSP
+        var certs = tlsSocket.getPeerCertificate(true);
+        tlsSocket.destroy();
+        ocsp.check({
+          cert: certs.raw,
+          issuer: certs.issuerCertificate.raw
+        }, function(err, res) {
+          // Handle invalid OCSP
+          if (err) {
+            callback(true, hasOCSP, false, err.message);
+          } else if(res && res.type && res.type === 'good') {
+            callback(true, hasOCSP, true);
+          } else {
+            callback(true, hasOCSP, false);
+          }
+        });
+      } else {
+        // No OCSP
+        callback(true, hasOCSP, false);
+      }
+    } else {
+      // Not valid TLS
+      callback(false, tlsSocket.authorizationError, false);
+    }
+
+  });
+
+  tlsSocket.on('error', function(err) {
+    tlsSocket.destroy();
+    callback(false, err, false);
+  });
+}
+
+// Define how we handle OCSP runtime vs test mode
+exports.test = function(url, local, callback) {
+  if(local) {
+    localTestOCSP(url, callback);
+  } else {
+    remoteTestOCSP(url, callback);
+  }
+};

+ 56 - 0
heimdall-library/lib/openssl.js

@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+'use strict';
+var exports = module.exports = {};
+
+const fs = require('fs');
+const { spawn } = require('child_process');
+
+// Places openssl might be
+const searchOpenSSLs = {
+  darwin: ['/usr/local/opt/openssl/bin/openssl'],
+  linux: ['/usr/bin/openssl', '/usr/local/bin/openssl', '/bin/openssl'],
+  win32: ['C:\\OpenSSL-Win64\\bin\\openssl.exe', 'C:\\OpenSSL-Win32\\bin\\openssl.exe']
+};
+
+// Search for where openssl might be, default to path
+function findOpenSSL() {
+    var searchOpenSSL = searchOpenSSLs[process.platform];
+    for(var attempt of searchOpenSSL) {
+      if(fs.existsSync(attempt)) {
+        return attempt;
+      }
+    }
+
+    // Try path
+    return 'openssl';
+}
+
+// Function for controlling openssl cli, to be passed to heimdall
+exports.openssl = function(parameters, callback, input) {
+  // By default generate a weak dhkey to test
+  if(!parameters) {
+    parameters = ['dhparam', '-dsaparam', '256'];
+  }
+
+  var openssl = spawn(findOpenSSL(), parameters);
+  var rData = '';
+
+  openssl.stdout.on('data', data => {
+      rData += data.toString('utf8');
+  });
+
+  openssl.on('error', function(err) {});
+
+  openssl.on('close', function(code) {
+    if(code === 0 && rData) {
+      callback(true, rData);
+    } else {
+      callback(false, null);
+    }
+  });
+
+  if(input) {
+    openssl.stdin.write(input);
+    openssl.stdin.end();
+  }
+}

+ 29 - 0
heimdall-library/modules.js

@@ -0,0 +1,29 @@
+'use strict';
+
+var exports = module.exports = {};
+
+
+// Passive
+exports['csp'] = require('./modules/passive/csp.js');
+exports['hsts'] = require('./modules/passive/hsts.js');
+exports['https'] = require('./modules/passive/https.js');
+exports['server'] = require('./modules/passive/server.js');
+exports['versionnumbers'] = require('./modules/passive/versionnumbers.js');
+exports['xxssprotection'] = require('./modules/passive/xxssprotection.js');
+exports['referrerpolicy'] = require('./modules/passive/referrerpolicy.js');
+exports['xcontenttypeoptions'] = require('./modules/passive/xcontenttypeoptions.js');
+exports['xframeoptions'] = require('./modules/passive/xframeoptions.js');
+
+if(!process.browser) {
+  // Active
+  exports['http'] = require('./modules/active/http.js');
+  exports['dns'] = require('./modules/active/dns.js');
+  exports['tlsciphers'] = require('./modules/active/tlsciphers.js');
+  exports['tlspfs'] = require('./modules/active/tlspfs.js');
+  exports['tlsocsp'] = require('./modules/active/tlsocsp.js');
+  exports['tlscert'] = require('./modules/active/tlscert.js');
+
+  // Aggressive
+  exports['heartbleed'] = require('./modules/aggressive/heartbleed.js');
+  exports['ports'] = require('./modules/aggressive/ports.js');
+}

+ 145 - 0
heimdall-library/modules/active/dns.js

@@ -0,0 +1,145 @@
+'use strict';
+
+var exports = module.exports = {};
+const url = require('url');
+const packet = require('dns-packet');
+const dns = require('dns');
+const hDNS = require('../../lib/local-dns.js');
+
+/*
+var states = {
+  0: 'U',
+  1: 'F',
+  2: 'D',
+  3: 'C',
+  4: 'B',
+  5: 'A',
+  6: 'A+'
+};
+*/
+
+/*
+var types = {
+  0: 'passive',
+  1: 'active',
+  2: 'aggressive'
+};
+*/
+
+exports.lowestState = 5;
+exports.name = 'Domain Name System';
+exports.type = 1;
+exports.depends = [];
+exports.link = 'https://en.wikipedia.org/wiki/Domain_Name_System';
+exports.description = 'Tests for the presence of DS and CAA DNS records.';
+
+// Send the dns request
+function sendDNS(dnsServer, dnsType, request, localServer, callback) {
+  var hasRecord = false;
+  var closed = false;
+  var client = hDNS.Socket(localServer);
+
+  var sentCallback = false;
+
+  var errTim = function(err) {
+    if(!closed) {
+      client.end();
+      client.destroy();
+      closed = true;
+      client = null;
+    }
+  };
+
+  var endFunc = function() {
+    if(!sentCallback) {
+      sentCallback = true;
+      callback(hasRecord);
+    }
+  };
+
+  client.on('data', function (data) {
+    if(data) {
+      var dData = null;
+      if(data instanceof Buffer) {
+        dData = packet.streamDecode(data);
+      } else {
+        dData = data;
+      }
+
+      // Parse answers for matching type
+      if(dData && dData.hasOwnProperty('answers') && dData.answers && dData.answers.length) {
+        for(var answer of dData.answers) {
+          if(answer && answer.hasOwnProperty('type') && answer.hasOwnProperty('data') && answer.type === dnsType) {
+            hasRecord = true;
+            break;
+          }
+        }
+      }
+    }
+    errTim();
+  });
+
+  client.setTimeout(1000);
+  client.on('close', endFunc);
+  client.on('end', endFunc);
+  client.on('error', errTim);
+  client.on('timeout', errTim);
+
+  client.connect(53, dnsServer, function () {
+    client.write(request);
+  });
+}
+
+function hasRecord(dnsType, hostname, dnsServers, localServer, callback) {
+  // Build request packet
+  const request = packet.streamEncode({
+    type: 'query',
+    flags: packet.RECURSION_DESIRED,
+    questions: [{
+      type: dnsType,
+      name: hostname
+    }]
+  });
+
+  // Get system dns servers if not defined
+  if(!dnsServers) {
+    dnsServers = dns.getServers();
+  }
+
+  // Send DNS
+  sendDNS(dnsServers[0], dnsType, request, localServer, function(gotResponse){
+    // Iterate DNS servers
+    dnsServers.shift();
+    // If we got a response, use it, else use next dns server
+    if(!gotResponse && dnsServers.length) {
+      hasRecord(dnsType, hostname, dnsServers, localServer, callback);
+    } else {
+      callback(gotResponse);
+    }
+  });
+}
+
+exports.run = function(object) {
+  return new Promise(function(resolve, reject) {
+    const oURL = url.parse(object.url);
+    var result = {code: 5, message: 'No CAA', name: exports.name};
+    // Handle info mode
+    if(object.info) {
+        result.link = exports.link;
+        result.description = exports.description;
+    }
+    // Check for CAA and DS record types
+    hasRecord('CAA', oURL.hostname, null, object.dnsServer, function(hasCAA) {
+      hasRecord('DS', oURL.hostname, null, object.dnsServer, function(hasDS) {
+        if(hasCAA && hasDS) {
+          result.code = 6;
+          delete result.message;
+        } else if(hasCAA) {
+          result.code = 5;
+          result.message = 'No DNSSEC';
+        }
+        resolve(result);
+      });
+    });
+  });
+}

+ 101 - 0
heimdall-library/modules/active/http.js

@@ -0,0 +1,101 @@
+'use strict';
+
+var exports = module.exports = {};
+const url = require('url');
+const http = require('http');
+
+/*
+var states = {
+  0: 'U',
+  1: 'F',
+  2: 'D',
+  3: 'C',
+  4: 'B',
+  5: 'A',
+  6: 'A+'
+};
+*/
+
+/*
+var types = {
+  0: 'passive',
+  1: 'active',
+  2: 'aggressive'
+};
+*/
+
+exports.lowestState = 3;
+exports.name = 'HTTP';
+exports.type = 1;
+exports.depends = ['HTTPS'];
+exports.link = 'https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol';
+exports.description = 'Tests for a redirect from HTTP to HTTPS.';
+
+
+exports.run = function(object) {
+  return new Promise(function(resolve, reject) {
+    var result = {code: 1, message: 'No 301 redirect from http to https', name: exports.name};
+    if(object.info) {
+        result.link = exports.link;
+        result.description = exports.description;
+    }
+
+    // Check for valid url
+    const oURL = url.parse(object.url);
+    if(!oURL.hostname) {