├── .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 |
--------------------------------------------------------------------------------