├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── scripts ├── export ├── service ├── setup └── start └── src ├── Config.iced ├── Crypto.iced ├── Diff.iced ├── Event.iced ├── Export.iced ├── Main.iced ├── Mods.iced ├── PolicyAPI.iced ├── Sandbox.iced ├── Setup.iced └── Vault.iced /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | policies 40 | *.pub.asc 41 | .localstorage 42 | rethinkdb_data 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stampery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://trailbot.io) 2 | 3 | # [Trailbot](https://trailbot.io) Watcher DEVELOPER PREVIEW 4 | 5 | __Trailbot tracks files and logs in your servers__, triggers [__Smart Policies__](https://github.com/trailbot/client/wiki/Smart-Policies) upon __tampering__ and generates an __immutable audit trail__ of everything happening to them. 6 | 7 | [Smart Policies](https://github.com/trailbot/client/wiki/Smart-Policies) are simple scripts that get called every time a tracked file changes. They trigger actions such as emailing someone, rolling files back or even shutting the system down. There are [plenty of them ready to use](https://github.com/trailbot/client/wiki/Smart-Policies#ready-to-use-policies), and you can even [create your own](https://github.com/trailbot/client/wiki/Smart-Policies). 8 | 9 | Trailbot has three components: 10 | + [__Watcher__](https://github.com/trailbot/watcher): (this repository) a server daemon that monitors your files and logs, registers file events and enforces [smart policies](https://github.com/trailbot/client/wiki/Smart-Policies). 11 | + [__Client__](https://github.com/trailbot/client): desktop app for managing watchers, defining policies and reading file events. 12 | + [__Vault__](https://github.com/trailbot/vault): a backend that works as a relay for the watcher's settings and the server events. 13 | 14 | # Why Trailbot? 15 | 16 | Current security solutions are based on an obsolete paradigm: building walls and fences. Companies advertise their overcomplicated perimeter security systems as if they were impenetrable. But even so, we hear everyday about __cyber security breaches__ at even the largest corporations. 17 | 18 | Moreover, they will not protect you at all from internal breaches and __insider threats__. Furthermore, most data resides nowadays in the cloud, where walls, border and fences fade and blur. 19 | 20 | With Trailbot, you can rest assured of the __integrity of your data__, being it a system log or any other important file. It doesn't matter if an outsider got access to your systems or an insider decided to go rogue—__you are now in control__. 21 | 22 | # Installation 23 | 24 | Please refer to our [Getting Started guide](https://github.com/trailbot/client/blob/master/GETTING-STARTED.md) for detailed installation instructions. 25 | 26 | # Get Involved 27 | 28 | We'd love for you to help us build Trailbot. If you'd like to be a contributor, check out our [Contributing guide](https://github.com/trailbot/client/blob/master/CONTRIBUTING.md). 29 | 30 | # FAQ 31 | 32 | Check out our [FAQ at the wiki](https://github.com/trailbot/client/wiki/FAQ). 33 | 34 | [](https://stampery.com) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trailbot-watcher", 3 | "version": "0.3.2", 4 | "dependencies": { 5 | "@horizon/client": "~1.1.3", 6 | "chokidar": "*", 7 | "coffee-script": "*", 8 | "colors": "*", 9 | "diff": "*", 10 | "grunt": "*", 11 | "grunt-cli": "*", 12 | "grunt-iced-coffee": "*", 13 | "iced-coffee-script": "*", 14 | "iced-error": "*", 15 | "iced-runtime": "*", 16 | "inquirer": "*", 17 | "kbpgp": "*", 18 | "mkdirp": "*", 19 | "node-localstorage": "*", 20 | "npm": "*", 21 | "pgp-word-list-converter": "*", 22 | "progress": "*", 23 | "simple-git": "*", 24 | "sleep": "*", 25 | "spawn-npm-install": "*", 26 | "vm2": "*", 27 | "watch": "*", 28 | "ws": "^0.8.1" 29 | }, 30 | "engines": { 31 | "node": "5.x" 32 | }, 33 | "devDependencies": { 34 | "grunt-concurrent": "*", 35 | "grunt-contrib-watch": "*", 36 | "grunt-githooks": "*", 37 | "grunt-nodemon": "*", 38 | "ngrok": "*" 39 | }, 40 | "scripts": { 41 | "start": "./scripts/start", 42 | "export": "./scripts/export", 43 | "service": "./scripts/service", 44 | "setup": "./scripts/setup && ./scripts/service" 45 | }, 46 | "bin": { 47 | "trailbot-watcher": "./scripts/start" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | require('iced-coffee-script/register'); 4 | const debug = require('debug')('trailbot-watcher'), 5 | app = require('../src/Export') 6 | -------------------------------------------------------------------------------- /scripts/service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Installing service..."; 3 | touch /var/log/trailbot-watcher.log > /dev/null 2>&1 && 4 | ( 5 | # Systemd 6 | if [ $(which systemd >/dev/null 2>&1) ] 7 | then 8 | 9 | cat > /etc/systemd/system/trailbot-watcher.service <<- EOM 10 | [Unit] 11 | Description=Trailbot Watcher 12 | Wants=network-online.target 13 | After=network.target network-online.target 14 | 15 | [Service] 16 | Type=simple 17 | ExecStart=/bin/sh -c "$(pwd)/scripts/start >> /var/log/trailbot-watcher.log 2>&1" 18 | Restart=always 19 | User=root 20 | Group=root 21 | WorkingDirectory=$(pwd) 22 | 23 | [Install] 24 | WantedBy=multi.user.target 25 | EOM 26 | systemctl enable trailbot-watcher 27 | 28 | else 29 | 30 | # Upstart 31 | if [ $(which upstart >/dev/null 2>&1) ] 32 | then 33 | 34 | cat > /etc/init/trailbot-watcher.conf <<- EOM 35 | #!upstart 36 | description "Trailbot Watcher" 37 | 38 | respawn 39 | 40 | start on (local-filesystems and net-device-up IFACE!=lo) 41 | stop on shutdown 42 | 43 | script 44 | chdir $(pwd) 45 | exec /bin/sh -c "$(pwd)/scripts/start >> /var/log/trailbot-watcher.log 2>&1" 46 | end script 47 | EOM 48 | chmod +x /etc/init/trailbot-watcher.conf 49 | 50 | # System V 51 | else 52 | 53 | cat > /etc/init.d/trailbot-watcher <<- EOM 54 | #!/bin/bash 55 | # 56 | # trailbot-watcher Start up Trailbot Watcher 57 | # 58 | # chkconfig: 2345 55 25 59 | # processname: trailbot-watcher 60 | # pidfile: /var/run/trailbot-watcher.pid 61 | 62 | ### BEGIN INIT INFO 63 | # Provides: trailbot-watcher 64 | # Required-Start: \$local_fs \$network \$syslog 65 | # Required-Stop: \$local_fs \$syslog 66 | # Should-Start: \$syslog 67 | # Should-Stop: \$network \$syslog 68 | # Default-Start: 2 3 4 5 69 | # Default-Stop: 0 1 6 70 | ### END INIT INFO 71 | 72 | # Source function library. 73 | . /etc/init.d/functions 74 | 75 | PIDFILE="/var/run/trailbot-watcher.pid" 76 | 77 | start() { 78 | cd $(pwd) 79 | /bin/sh -c $(pwd)/scripts/start >> /var/log/trailbot-watcher.log 2>&1 & 80 | echo \$! > \$PIDFILE 81 | } 82 | 83 | stop() { 84 | killproc -p \$PIDFILE >/dev/null 2>&1 85 | } 86 | 87 | case "\$1" in 88 | start) 89 | start 90 | ;; 91 | stop) 92 | stop 93 | ;; 94 | restart|reload|force-reload) 95 | stop 96 | start 97 | ;; 98 | *) 99 | echo \$"Usage: \$0 {start|stop|restart|reload|force-reload}" 100 | exit 2 101 | esac 102 | 103 | EOM 104 | chmod +x /etc/init.d/trailbot-watcher 105 | 106 | fi 107 | fi 108 | ) && ( 109 | service trailbot-watcher restart && 110 | echo "Trailbot Watcher is now up and running!" 111 | ) || echo -e "\e[01;31mERROR: Could not install service.\e[0m" 112 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | require('iced-coffee-script/register'); 4 | const debug = require('debug')('trailbot-watcher'), 5 | app = require('../src/Setup') 6 | -------------------------------------------------------------------------------- /scripts/start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | require('iced-coffee-script/register'); 4 | const debug = require('debug')('trailbot-watcher'), 5 | app = require('../src/Main') 6 | -------------------------------------------------------------------------------- /src/Config.iced: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | {make_esc} = require 'iced-error' 3 | colors = require 'colors' 4 | 5 | Config = {} 6 | 7 | Config.local_storage = '.localstorage' 8 | Config.vault = 'vault.trailbot.io:8443' 9 | Config.policies_dir = './policies' 10 | Config.secure = true 11 | 12 | if process.env['DEV'] is 'true' 13 | console.log 'DEV MODE' 14 | Config.vault = 'localhost:8443' 15 | Config.secure = false 16 | 17 | localStorage = new (require 'node-localstorage').LocalStorage(Config.local_storage) 18 | settings = localStorage._metaKeyMap 19 | 20 | for key of settings 21 | Config[key] = localStorage.getItem key 22 | 23 | if Config.vault? && Config.watcher_priv_key? && Config.client_pub_key? 24 | Config.ready = true 25 | 26 | Config.header = "888888888888888888b. d8888 88888 888 " + "888888b. .d88888b. 88888888888".red + 27 | "\n 888 888 Y88b d88888 888 888 " + "888 \"88b d88P\" \"Y88b 888 ".red + 28 | "\n 888 888 888 d88P888 888 888 " + "888 .88P 888 888 888 ".red + 29 | "\n 888 888 d88P d88P 888 888 888 " + "8888888K. 888 888 888 ".red + 30 | "\n 888 8888888P\" d88P 888 888 888 " + "888 \"Y88b888 888 888 ".red + 31 | "\n 888 888 T88b d88P 888 888 888 " + "888 888888 888 888 ".red + 32 | "\n 888 888 T88b d8888888888 888 888 " + "888 d88PY88b. .d88P 888 ".red + 33 | "\n 888 888 T88bd88P 888 88888 88888888" + "8888888P\" \"Y88888P\" 888".red + 34 | "\n" 35 | 36 | module.exports = Config 37 | -------------------------------------------------------------------------------- /src/Crypto.iced: -------------------------------------------------------------------------------- 1 | {make_esc} = require 'iced-error' 2 | 3 | fs = require 'fs' 4 | kbpgp = require 'kbpgp' 5 | extend = require('util')._extend 6 | {Literal} = require '../node_modules/kbpgp/lib/openpgp/packet/literal' 7 | 8 | class Crypto 9 | constructor : (watcherArmored, clientArmored, cb) -> 10 | esc = make_esc (err) -> console.error "[CRYPTO] #{err}" 11 | 12 | 13 | wKey = {armored: watcherArmored} 14 | 15 | await kbpgp.KeyManager.import_from_armored_pgp wKey, esc defer @watcherKey 16 | if @watcherKey.is_pgp_locked() 17 | await @watcherKey.unlock_pgp { passphrase: '' }, esc defer() 18 | 19 | @ring = new kbpgp.keyring.KeyRing 20 | @ring.add_key_manager @watcherKey 21 | 22 | if clientArmored 23 | mKey = {armored: clientArmored} 24 | await kbpgp.KeyManager.import_from_armored_pgp mKey, esc defer @clientKey 25 | @ring.add_key_manager @clientKey 26 | 27 | cb this 28 | 29 | encrypt : (data, filename, cb) => 30 | return setTimeout @encrypt.bind(this, data, cb), 500 if not @watcherKey 31 | 32 | params = 33 | encrypt_for : @clientKey 34 | sign_with : @watcherKey 35 | literals: [ new Literal 36 | format: kbpgp.const.openpgp.literal_formats.utf8 37 | filename: new Buffer(filename) 38 | date: kbpgp.util.unix_time() 39 | data: new Buffer(data) 40 | ] 41 | kbpgp.box params, cb 42 | 43 | decrypt : (data, cb) => 44 | esc = make_esc (err) -> console.error "[CRYPTO] #{err}" 45 | 46 | params = 47 | keyfetch: @ring 48 | armored: data.content 49 | await kbpgp.unbox params, esc defer literals 50 | delete data.content 51 | cb extend data, JSON.parse literals[0].toString() 52 | 53 | module.exports = Crypto 54 | -------------------------------------------------------------------------------- /src/Diff.iced: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | diff = require 'diff' 4 | sleep = require('sleep').sleep 5 | 6 | class Diff 7 | constructor : (@filepath) -> 8 | @range = 5 9 | @filename = path.basename @filepath 10 | await @update defer(err, chunks) 11 | if err 12 | console.error "Error monitoring #{@filepath}: file does not exist" 13 | else 14 | console.log "Monitoring #{@filepath}" 15 | 16 | update : (force = false, cb) -> 17 | if @cur? 18 | @prev = @cur 19 | else 20 | await fs.readFile @filepath, {encoding: 'utf8'}, defer err, data 21 | @cur = 22 | content: !err? && data || '' 23 | time: Date.now() 24 | return 25 | await fs.readFile @filepath, {encoding: 'utf8'}, defer err, data 26 | if force 27 | while data is '' 28 | console.log "[DIFF] Delayed read (NodeJS is so weird)" 29 | data = fs.readFileSync @filepath, 'utf8' 30 | sleep 1 31 | @cur = 32 | content: !err? && data || '' 33 | time: Date.now() 34 | res = diff.diffLines @prev.content, @cur.content 35 | return unless res.length > 1 or (res[0].added? or res[0].removed?) 36 | 37 | offset = 1 38 | chunks = [] 39 | res.forEach (cur, i, a) => 40 | type = (cur.added && 'add') || (cur.removed && 'rem') 41 | if type 42 | chunks.push 43 | type: type 44 | start: offset 45 | lines: cur.value.split('\n').slice(0, cur.count) 46 | else 47 | if cur.count > @range 48 | size = Math.floor(@range / 2 ) 49 | if i > 0 50 | chunks.push 51 | type: 'fill' 52 | start: offset 53 | lines: cur.value.split('\n').slice(0, size + 1) 54 | offset += size 55 | chunks.push 56 | type: 'ellipsis' 57 | size: cur.count - size 58 | offset += cur.count - size 59 | if i < a.length - 1 60 | chunks.push 61 | type: 'fill' 62 | start: offset 63 | lines: cur.value.split('\n').slice(-size-1, -1) 64 | offset += size 65 | offset -= cur.count 66 | else 67 | chunks.push 68 | type: 'fill' 69 | start: offset 70 | lines: cur.value.split('\n').slice(0, cur.count) 71 | offset += cur.count 72 | cb and cb null, chunks 73 | 74 | module.exports = Diff 75 | -------------------------------------------------------------------------------- /src/Event.iced: -------------------------------------------------------------------------------- 1 | class Event 2 | 3 | constructor : (@type, {@creator, @reader, @path, @payload}) -> 4 | @ref = Date.now() 5 | this 6 | 7 | encrypt : (crypto, cb) => 8 | await crypto.encrypt @toString(), @path, defer err, @encrypted 9 | cb err, @encrypted if cb? 10 | 11 | toString : => 12 | JSON.stringify 13 | type: @type 14 | payload: @payload 15 | 16 | save : (vault, cb) => 17 | vault.save 'events', 18 | ref: @ref 19 | creator: @creator 20 | reader: @reader 21 | content: @encrypted 22 | datetime: new Date() 23 | v: 1 24 | , cb 25 | 26 | module.exports = Event 27 | -------------------------------------------------------------------------------- /src/Export.iced: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Config = require './Config' 4 | inquirer = require 'inquirer' 5 | colors = require 'colors' 6 | fs = require 'fs' 7 | 8 | class Exporter 9 | 10 | constructor : -> 11 | unless Config.watcher_pub_key 12 | console.error 'This watcher is not yet configured. Please run this command first:'.red 13 | console.error 'sudo npm run-script configure'.cyan.bold 14 | return 15 | 16 | if process.argv[2] 17 | console.log "indice argv ", process.argv[2] 18 | await fs.writeFile process.argv[2], Config.watcher_pub_key, {encoding: 'utf8'}, defer err, res 19 | if err 20 | console.error 'Invalid output path: directory does not exist or writing to it is not allowed.'.red 21 | else 22 | console.log "Public key succesfully exported to #{process.argv[2]}".green 23 | return 24 | 25 | 26 | inquirer.prompt [ 27 | name: 'output' 28 | message: "What method do you want to use for exporting your watcher's public key?" 29 | type: 'list' 30 | choices: [ 31 | name: 'Print to screen' 32 | value: 'stdio' 33 | , 34 | name: 'Write to filesystem' 35 | value: 'filesystem' 36 | ] 37 | ] 38 | .then ({output}) -> 39 | if output is 'stdio' 40 | # console.log Config.watcher_pub_key 41 | console.log "\nSentence:" 42 | console.log "#{Config.sentence}\n".cyan.bold 43 | else if output is 'filesystem' 44 | inquirer.prompt [ 45 | name: 'path' 46 | message: 'Path of the output file:' 47 | type: 'input' 48 | default: './trailbot_watcher.pub.asc' 49 | validate: (path) -> 50 | new Promise (next) -> 51 | console.log 52 | await fs.writeFile path, Config.sentence, {encoding: 'utf8'}, defer err, res 53 | # await fs.writeFile path, Config.watcher_pub_key, {encoding: 'utf8'}, defer err, res 54 | next err && 'Invalid output path: directory does not exist or writing permission is not granted.' || true 55 | ] 56 | .then ({path}) -> 57 | console.log "Public key succesfully exported to #{path}".green 58 | else if output is 'scp' 59 | console.error 'Copying to another system over scp is planned but not yet supported.'.bgRed 60 | console.error 'Please select another method.'.bgRed 61 | setTimeout Exporter, 2000 62 | 63 | new Exporter() 64 | -------------------------------------------------------------------------------- /src/Main.iced: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | fs = require 'fs' 4 | path = require 'path' 5 | chokidar = require 'chokidar' 6 | extend = require('util')._extend 7 | 8 | EventEmitter = require 'events' 9 | Config = require './Config' 10 | Crypto = require './Crypto' 11 | Diff = require './Diff' 12 | Vault = require './Vault' 13 | Sandbox = require './Sandbox' 14 | Event = require './Event' 15 | Mods = require './Mods' 16 | 17 | process.on 'uncaughtException', (err) -> 18 | console.error err.stack 19 | if err.message.indexOf 'transport close' > -1 20 | console.log '[WATCHER] Node exiting. If process is supervised, it will be respawned shortly.' 21 | process.exit 1 22 | else 23 | console.log '[WATCHER] Node NOT exiting...' 24 | return 25 | 26 | app = class App extends EventEmitter 27 | constructor : -> 28 | @localStorage = new (require 'node-localstorage').LocalStorage(Config.local_storage) 29 | @watcher = null 30 | @defaults = 31 | files: {} 32 | policies: [] 33 | 34 | # Initialize mods 35 | mods = @localStorage.getItem 'mods' 36 | if mods and mods.length 37 | mods = JSON.parse mods 38 | if mods 39 | for mod in mods 40 | if Mods[mod]? 41 | new Mods[mod](this) 42 | 43 | await new Crypto Config.watcher_priv_key, Config.client_pub_key, defer @cryptoBox 44 | @watcherFP = @cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') 45 | @clientFP = @cryptoBox.clientKey.get_pgp_fingerprint().toString('hex') 46 | console.log '[WATCHER] Watcher fingerprint:', @watcherFP 47 | console.log '[WATCHER] Client fingerprint', @clientFP 48 | 49 | await new Vault this, Config.vault, @watcherFP, defer @vault 50 | @emit 'vaultConnected' 51 | console.log '[WATCHER] Connected to vault' 52 | 53 | @vault.watch 'settings', {reader: @watcherFP, creator: @clientFP}, (settings) => 54 | console.log settings 55 | if settings 56 | @emit 'newSettings' 57 | if settings.content 58 | await @cryptoBox.decrypt settings, defer settings 59 | @processSettings settings 60 | 61 | processSettings : (settings) => 62 | console.log '[WATCHER] New settings:', settings 63 | 64 | @files = {} 65 | Object.keys(settings.files).map (key) => 66 | p = path.normalize key 67 | @files[p] = extend settings.files[key], {} 68 | @files[p].differ = new Diff p 69 | @files[p].policies = @files[p].policies.map (policy) -> 70 | policy.params.path = p 71 | console.log "[WATCHER] Creating Sandbox '#{policy.name}' (#{policy.uri}) for #{p}" 72 | policy.sandbox = new Sandbox policy, p 73 | policy 74 | 75 | if @watcher 76 | @watcher.close() 77 | 78 | @watcher = chokidar.watch Object.keys @files, 79 | awaitWriteFinish: true, 80 | atomic: true 81 | 82 | @watcher 83 | .on 'ready', () => 84 | console.log '[WATCHER] Ready for changes!' 85 | .on 'change', (path, stats) => @eventProcessor 'change', path, stats 86 | .on 'unlink', (path, stats) => @eventProcessor 'unlink', path, stats 87 | .on 'add', (path, stats) => @eventProcessor 'add', path, stats 88 | 89 | eventProcessor : (type, path, stats) => 90 | file = @files[path] 91 | console.log "[WATCHER] #{type} detected in #{path}" 92 | 93 | force = type is 'change' 94 | await file.differ.update force, defer err, diff 95 | 96 | event = new Event type, 97 | path: path 98 | creator: @watcherFP 99 | reader: @clientFP 100 | payload: type is 'change' and diff or undefined 101 | await event.encrypt @cryptoBox, defer() 102 | event.save @vault 103 | 104 | {prev, cur} = file.differ 105 | for policy in file.policies 106 | unless policy.paused 107 | console.log "[WATCHER] Enforcing policy #{policy.sandbox.name}" 108 | policy.sandbox.send {diff, prev, cur} 109 | 110 | 111 | module.exports = new app() 112 | -------------------------------------------------------------------------------- /src/Mods.iced: -------------------------------------------------------------------------------- 1 | request = require 'request' 2 | 3 | class Azure 4 | constructor : (@app) -> 5 | token = @stamperyToken() 6 | 7 | @app.defaults.policies.push 8 | name: 'Blockchain anchoring' 9 | uri: 'https://github.com/trailbot/stamper-policy' 10 | ref: 'master' 11 | lang: 'icedcoffeescript' 12 | params: 13 | secret: token 14 | proofsDir: '/var/proofs' 15 | 16 | stamperyToken : () => 17 | (@app.localStorage.getItem 'stampery_token') or @stamperySignup() 18 | 19 | stamperySignup : () => 20 | hostname = require('os').hostname() 21 | await request 22 | method: 'post' 23 | url: 'https://api-dashboard.stampery.com/trialToken' 24 | json: true 25 | data: 26 | email: @app.localStorage.getItem 'user_email' 27 | name: "Trailbot at #{hostname}" 28 | tags: ['trailbot', 'trial'] 29 | , defer err, res 30 | 31 | module.exports = 32 | azure: Azure 33 | -------------------------------------------------------------------------------- /src/PolicyAPI.iced: -------------------------------------------------------------------------------- 1 | Config = require './Config' 2 | Crypto = require './Crypto' 3 | Vault = require './Vault' 4 | Event = require './Event' 5 | 6 | class PolicyAPI 7 | 8 | constructor: (@path)-> 9 | @localStorage = new (require 'node-localstorage').LocalStorage(Config.local_storage) 10 | await new Crypto Config.watcher_priv_key, Config.client_pub_key, defer @cryptoBox 11 | @watcherFP = @cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') 12 | @clientFP = @cryptoBox.clientKey.get_pgp_fingerprint().toString('hex') 13 | await new Vault this, Config.vault, @watcherFP, defer @vault 14 | 15 | raise: (type, payload, cb) => 16 | #TODO may be instead of checkink here we should have a list of types not allowed 17 | if type.toLowerCase() != 'change' || 'link' != type.toLowerCase() || type.toLowerCase() != 'unlink' 18 | event = new Event type, 19 | path: @path 20 | creator: @watcherFP 21 | reader: @clientFP 22 | payload: payload 23 | await event.encrypt @cryptoBox, defer() 24 | event.save @vault, cb 25 | 26 | module.exports = PolicyAPI 27 | -------------------------------------------------------------------------------- /src/Sandbox.iced: -------------------------------------------------------------------------------- 1 | url = require 'url' 2 | path = require 'path' 3 | fs = require 'fs' 4 | mkdirp = require 'mkdirp' 5 | npmInstall = require 'spawn-npm-install' 6 | vm = require 'vm' 7 | crypto = require 'crypto' 8 | extend = require('util')._extend 9 | Config = require './Config' 10 | PolicyAPI = require './PolicyAPI' 11 | 12 | class Sandbox 13 | constructor : (repo, @file, cb) -> 14 | # Set policy uri, ref, name and path 15 | @uri = repo.uri 16 | @ref = repo.ref or 'master' 17 | @lang = repo.lang or 'javascript' 18 | @ext = { 19 | 'javascript': 'js' 20 | 'coffeescript': 'coffee' 21 | 'icedcoffeescript': 'iced' 22 | }[@lang] 23 | @queue = [] 24 | @ready = false 25 | 26 | @id = crypto 27 | .createHash 'md5' 28 | .update "#{@file}:#{@uri}:#{JSON.stringify repo.params}" 29 | .digest 'hex' 30 | .slice 0, 7 31 | 32 | @name = url.parse(@uri).pathname.slice(1).replace(/\.git/g, '') 33 | if @ref isnt 'master' 34 | @name += "/#{ref}" 35 | @path = path.normalize "#{Config.policies_dir}/#{@name}-#{@id}" 36 | @abs = path.resolve @path 37 | 38 | # Make sure that path exists 39 | await mkdirp @path, defer err 40 | # Pull repository contents 41 | await @pull defer() 42 | # Install dependencies 43 | await @install defer npmData 44 | console.log npmData 45 | #console.log "[NPM] Installed #{npmData.length} new packages" 46 | # Retrieve code and compile to JS 47 | await fs.readFile "#{@path}/main.#{@ext}", 'utf8', defer err, code 48 | code = @toJS code 49 | # Start virtualization 50 | @virtualize code, repo.params 51 | 52 | cb and cb this 53 | 54 | pull : (cb) -> 55 | console.log "[SANDBOX](#{@id}) Pulling repository contents from #{@uri}" 56 | @git = require('simple-git')(@path).init() 57 | await @git.getRemotes 'origin', defer err, remotes 58 | for remote in remotes 59 | @git.removeRemote(remote.name) if remote.name.length > 0 60 | @git 61 | .addRemote 'origin', @uri 62 | .fetch 'origin' 63 | .reset ["origin/#{@ref}",'--hard'] 64 | .checkout @ref 65 | .then cb 66 | 67 | toJS : (code) -> 68 | switch @lang 69 | when 'javascript' 70 | code 71 | when 'coffeescript' 72 | require('coffee-script').compile code, {header: false, bare: true} 73 | when 'icedcoffeescript' 74 | require('iced-coffee-script').compile code, {header: false, bare: true} 75 | 76 | install : (cb) => 77 | await fs.readFile "#{@abs}/package.json", 'utf8', defer err, json 78 | deps = Object.keys JSON.parse(json).dependencies 79 | console.log "[SANDBOX](#{@id}) Installing dependencies from #{@abs}/package.json", deps 80 | await npmInstall deps, {cwd: @abs}, defer err 81 | cb err or "[SANDBOX](#{@id}) Successfully installed #{deps}" 82 | 83 | virtualize : (code, params) => 84 | @vm = vm.createContext 85 | require: (mod) => 86 | try 87 | require mod 88 | catch e 89 | require "#{@abs}/node_modules/#{mod}" 90 | console: 91 | log: (first, others...) => 92 | console.log "[POLICY][#{@name}][#{@file}](#{@id}):\n> #{first}", others... 93 | module: {} 94 | iced: require('iced-coffee-script').iced 95 | PolicyAPI: new PolicyAPI(@file) 96 | 97 | 98 | vm.runInContext code, @vm, 99 | displayErrors: true 100 | vm.runInContext "policy = new this.module.exports(#{JSON.stringify(params)})", @vm 101 | vm.runInContext "console.log('Ready!');", @vm 102 | 103 | @ready = true 104 | for payload in @queue 105 | @send payload 106 | @queue = [] 107 | 108 | send : (payload) => 109 | if @ready 110 | if @vm?.module?.exports? 111 | payload = extend payload, 112 | path: @file 113 | date: Date.now() 114 | vm.runInContext "policy.receiver(#{JSON.stringify(payload)})", @vm 115 | else 116 | console.log "[SANDBOX] #{@name or @uri} is not ready yet, queuing event (#{@queue.length + 1})" 117 | @queue.push payload 118 | 119 | module.exports = Sandbox 120 | -------------------------------------------------------------------------------- /src/Setup.iced: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Config = require './Config' 4 | Crypto = require './Crypto' 5 | inquirer = require 'inquirer' 6 | colors = require 'colors' 7 | fs = require 'fs' 8 | os = require 'os' 9 | kbpgp = require 'kbpgp' 10 | progress = require 'progress' 11 | pgpWordList = require('pgp-word-list-converter')() 12 | crypto = require 'crypto' 13 | Vault = require './Vault' 14 | 15 | 16 | class Configure 17 | 18 | constructor : -> 19 | @localStorage = new require 'node-localstorage' 20 | .LocalStorage(Config.local_storage) 21 | @done = false 22 | process.on 'exit', => 23 | unless @done 24 | process.exitCode = 1 25 | 26 | console.log Config.header.bold 27 | 28 | inquirer.prompt [ 29 | name: 'hostname' 30 | message: "Choose a name for this watcher" 31 | type: 'input' 32 | default: os.hostname() 33 | , 34 | name: 'vault' 35 | message: "Type the domain and port of the vault server you want to use" 36 | type: 'input' 37 | default: 'vault.trailbot.io:8443' 38 | ] 39 | .then (answers) => 40 | @alert "Ok, we are now generating a new PGP keypar for this watcher.", true 41 | @alert "This may take up to a couple of minutes. Please wait while the magic happens...\n " 42 | @progress = new progress ' Generating... [:bar] :percent'.bold, 43 | total: 330 44 | complete: '=' 45 | incomplete: ' ' 46 | width: 50 47 | await @keygen answers.hostname, defer watcher_priv_key, watcher_pub_key 48 | @localStorage.setItem 'watcher_priv_key', watcher_priv_key 49 | @localStorage.setItem 'watcher_pub_key', watcher_pub_key 50 | @localStorage.setItem 'vault', answers.vault 51 | 52 | await new Crypto watcher_priv_key, null, defer cryptoBox 53 | watcherFP = cryptoBox.watcherKey.get_pgp_fingerprint().toString('hex') 54 | 55 | exchange = 56 | channel: @generateChannel() 57 | creator: watcherFP 58 | watcher: watcher_pub_key 59 | expires: @getExpirationDate() 60 | 61 | @done = true 62 | console.log '\n' 63 | 64 | await new Vault this, answers.vault, watcherFP, defer vault 65 | await vault.save 'exchange', exchange, defer {id} 66 | process.exit 1 unless id 67 | exchange.id = id 68 | 69 | @alert "Now install Trailbot Client in your computer and start the setup wizard." , true 70 | @alert "The following 8 words will be required by Trailbot Client:" 71 | @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true 72 | 73 | @alert "Waiting for confirmation from Trailbot Client..." , true 74 | vault.watch 'exchange', exchange.id, (change) => 75 | # if change is null the document was deleted 76 | process.exit 0 unless change 77 | if change?.client 78 | @localStorage.setItem 'client_pub_key', change.client 79 | vault.remove 'exchange', [change.id], (res) => 80 | console.log "file deleted" 81 | 82 | # every 5 minutes generate new words 83 | setInterval => 84 | exchange.channel = @generateChannel() 85 | exchange.expires = @getExpirationDate() 86 | vault.replace 'exchange', exchange 87 | @alert "Time to get confirmation from Trailbot Client expired", true 88 | @alert "New words generated" 89 | @alert "#{@channelToWords(exchange.channel)}".cyan.bold, true 90 | , 300000 91 | 92 | 93 | 94 | 95 | keygen : (identity, cb, pcb) => 96 | opts = 97 | userid: "#{identity} " 98 | asp: new kbpgp.ASP 99 | progress_hook: => 100 | @progress.tick() unless @progress.complete 101 | await kbpgp.KeyManager.generate_rsa opts, defer err, key 102 | await key.sign {}, defer err 103 | await key.export_pgp_private {}, defer err, priv 104 | await key.export_pgp_public {}, defer err, pub 105 | cb priv, pub 106 | 107 | alert : (text, breakBefore) -> 108 | b = breakBefore and "\n" or "" 109 | console.log "#{b}! ".green + text.bold 110 | 111 | generateChannel : () => 112 | word = Math.random().toString(36).substring(2) 113 | crypto.createHash('md5').update(word).digest("hex").substr(0, 16) 114 | 115 | getExpirationDate : () => 116 | now = new Date() 117 | now.setMinutes(now.getMinutes() + 5) 118 | now.toString() 119 | 120 | channelToWords : (channel) => 121 | pgpWordList.toWords(channel).toString().replace(/,/g,' ') 122 | 123 | 124 | 125 | new Configure() 126 | -------------------------------------------------------------------------------- /src/Vault.iced: -------------------------------------------------------------------------------- 1 | Config = require './Config' 2 | Horizon = require '@horizon/client/dist/horizon' 3 | 4 | class Vault 5 | constructor : (app, host, watcherFP, cb) -> 6 | @app = app 7 | authType = @getToken() 8 | secure = Config.secure 9 | @hz = Horizon({host, authType, secure}) 10 | 11 | @hz.connect() 12 | @users = @hz 'users' 13 | @settings = @hz 'settings' 14 | @events = @hz 'events' 15 | @exchange = @hz 'exchange' 16 | 17 | @hz.onReady () => 18 | token = JSON.parse(@hz.utensils.tokenStorage._storage._storage.get('horizon-jwt')).horizon 19 | @app.localStorage.setItem 'horizon_jwt', token 20 | @hz.currentUser().fetch().subscribe (me) => 21 | me.data = 22 | key: watcherFP 23 | @users.replace me 24 | console.log 'Me:', me if @app.emit 25 | @app.emit 'vaultLoggedIn', me if @app.emit 26 | cb and cb this 27 | 28 | @hz.onDisconnected (e) => 29 | unless @retried 30 | @retried = true 31 | @app.localStorage.removeItem 'horizon_jwt' 32 | @constructor app, host, watcherFP, cb 33 | 34 | getToken : () -> 35 | jwt = @app.localStorage.getItem 'horizon_jwt' 36 | if jwt 37 | { token: jwt, storeLocally: false } 38 | else 39 | 'anonymous' 40 | 41 | save : (col, object, cb) -> 42 | console.log "Saving into #{col}" if @app.emit 43 | console.log 'SAVING', object if @app.emit 44 | this[col]?.store(object).subscribe(cb) 45 | 46 | replace : (col, object) -> 47 | console.log "Replacing into #{col}" if @app.emit 48 | this[col]?.replace object 49 | 50 | get : (col, query, cb) -> 51 | this[col]?.find(query).fetch().defaultIfEmpty().subscribe(cb) 52 | 53 | watch : (col, query, cb, err) -> 54 | this[col]?.find(query).watch().subscribe(cb, err) 55 | 56 | remove : (col, ids) -> 57 | console.log "Removing from #{col}" if @app.emit 58 | this[col].removeAll(ids) 59 | 60 | getCollection : () -> 61 | @exchange 62 | 63 | 64 | module.exports = Vault 65 | --------------------------------------------------------------------------------