immunio 0.15.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +234 -0
- data/README.md +147 -0
- data/bin/immunio +5 -0
- data/lib/immunio.rb +29 -0
- data/lib/immunio/agent.rb +260 -0
- data/lib/immunio/authentication.rb +96 -0
- data/lib/immunio/blocked_app.rb +38 -0
- data/lib/immunio/channel.rb +432 -0
- data/lib/immunio/cli.rb +39 -0
- data/lib/immunio/context.rb +114 -0
- data/lib/immunio/errors.rb +43 -0
- data/lib/immunio/immunio_ca.crt +45 -0
- data/lib/immunio/logger.rb +87 -0
- data/lib/immunio/plugins/action_dispatch.rb +45 -0
- data/lib/immunio/plugins/action_view.rb +431 -0
- data/lib/immunio/plugins/active_record.rb +707 -0
- data/lib/immunio/plugins/active_record_relation.rb +370 -0
- data/lib/immunio/plugins/authlogic.rb +80 -0
- data/lib/immunio/plugins/csrf.rb +24 -0
- data/lib/immunio/plugins/devise.rb +40 -0
- data/lib/immunio/plugins/environment_reporter.rb +69 -0
- data/lib/immunio/plugins/eval.rb +51 -0
- data/lib/immunio/plugins/exception_handler.rb +55 -0
- data/lib/immunio/plugins/gems_tracker.rb +5 -0
- data/lib/immunio/plugins/haml.rb +36 -0
- data/lib/immunio/plugins/http_finisher.rb +50 -0
- data/lib/immunio/plugins/http_tracker.rb +203 -0
- data/lib/immunio/plugins/io.rb +96 -0
- data/lib/immunio/plugins/redirect.rb +42 -0
- data/lib/immunio/plugins/warden.rb +66 -0
- data/lib/immunio/processor.rb +234 -0
- data/lib/immunio/rails.rb +26 -0
- data/lib/immunio/request.rb +139 -0
- data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
- data/lib/immunio/rufus_lua_ext/state.rb +157 -0
- data/lib/immunio/rufus_lua_ext/table.rb +137 -0
- data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
- data/lib/immunio/version.rb +5 -0
- data/lib/immunio/vm.rb +291 -0
- data/lua-hooks/ext/all.c +78 -0
- data/lua-hooks/ext/bitop/README +22 -0
- data/lua-hooks/ext/bitop/bit.c +189 -0
- data/lua-hooks/ext/extconf.rb +38 -0
- data/lua-hooks/ext/libinjection/COPYING +37 -0
- data/lua-hooks/ext/libinjection/libinjection.h +65 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
- data/lua-hooks/ext/libinjection/lualib.c +109 -0
- data/lua-hooks/ext/lpeg/HISTORY +90 -0
- data/lua-hooks/ext/lpeg/lpcap.c +537 -0
- data/lua-hooks/ext/lpeg/lpcap.h +43 -0
- data/lua-hooks/ext/lpeg/lpcode.c +986 -0
- data/lua-hooks/ext/lpeg/lpcode.h +34 -0
- data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
- data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
- data/lua-hooks/ext/lpeg/lpprint.c +244 -0
- data/lua-hooks/ext/lpeg/lpprint.h +35 -0
- data/lua-hooks/ext/lpeg/lptree.c +1238 -0
- data/lua-hooks/ext/lpeg/lptree.h +77 -0
- data/lua-hooks/ext/lpeg/lptypes.h +149 -0
- data/lua-hooks/ext/lpeg/lpvm.c +355 -0
- data/lua-hooks/ext/lpeg/lpvm.h +58 -0
- data/lua-hooks/ext/lpeg/makefile +55 -0
- data/lua-hooks/ext/lpeg/re.html +498 -0
- data/lua-hooks/ext/lpeg/test.lua +1409 -0
- data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
- data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
- data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
- data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
- data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
- data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
- data/lua-hooks/ext/lua-snapshot/README.md +18 -0
- data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
- data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
- data/lua-hooks/ext/lua/COPYRIGHT +34 -0
- data/lua-hooks/ext/lua/lapi.c +1087 -0
- data/lua-hooks/ext/lua/lapi.h +16 -0
- data/lua-hooks/ext/lua/lauxlib.c +652 -0
- data/lua-hooks/ext/lua/lauxlib.h +174 -0
- data/lua-hooks/ext/lua/lbaselib.c +659 -0
- data/lua-hooks/ext/lua/lcode.c +831 -0
- data/lua-hooks/ext/lua/lcode.h +76 -0
- data/lua-hooks/ext/lua/ldblib.c +398 -0
- data/lua-hooks/ext/lua/ldebug.c +638 -0
- data/lua-hooks/ext/lua/ldebug.h +33 -0
- data/lua-hooks/ext/lua/ldo.c +519 -0
- data/lua-hooks/ext/lua/ldo.h +57 -0
- data/lua-hooks/ext/lua/ldump.c +164 -0
- data/lua-hooks/ext/lua/lfunc.c +174 -0
- data/lua-hooks/ext/lua/lfunc.h +34 -0
- data/lua-hooks/ext/lua/lgc.c +710 -0
- data/lua-hooks/ext/lua/lgc.h +110 -0
- data/lua-hooks/ext/lua/linit.c +38 -0
- data/lua-hooks/ext/lua/liolib.c +556 -0
- data/lua-hooks/ext/lua/llex.c +463 -0
- data/lua-hooks/ext/lua/llex.h +81 -0
- data/lua-hooks/ext/lua/llimits.h +128 -0
- data/lua-hooks/ext/lua/lmathlib.c +263 -0
- data/lua-hooks/ext/lua/lmem.c +86 -0
- data/lua-hooks/ext/lua/lmem.h +49 -0
- data/lua-hooks/ext/lua/loadlib.c +705 -0
- data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
- data/lua-hooks/ext/lua/lobject.c +214 -0
- data/lua-hooks/ext/lua/lobject.h +381 -0
- data/lua-hooks/ext/lua/lopcodes.c +102 -0
- data/lua-hooks/ext/lua/lopcodes.h +268 -0
- data/lua-hooks/ext/lua/loslib.c +243 -0
- data/lua-hooks/ext/lua/lparser.c +1339 -0
- data/lua-hooks/ext/lua/lparser.h +82 -0
- data/lua-hooks/ext/lua/lstate.c +214 -0
- data/lua-hooks/ext/lua/lstate.h +169 -0
- data/lua-hooks/ext/lua/lstring.c +111 -0
- data/lua-hooks/ext/lua/lstring.h +31 -0
- data/lua-hooks/ext/lua/lstrlib.c +871 -0
- data/lua-hooks/ext/lua/ltable.c +588 -0
- data/lua-hooks/ext/lua/ltable.h +40 -0
- data/lua-hooks/ext/lua/ltablib.c +287 -0
- data/lua-hooks/ext/lua/ltm.c +75 -0
- data/lua-hooks/ext/lua/ltm.h +54 -0
- data/lua-hooks/ext/lua/lua.c +392 -0
- data/lua-hooks/ext/lua/lua.def +131 -0
- data/lua-hooks/ext/lua/lua.h +388 -0
- data/lua-hooks/ext/lua/lua.rc +28 -0
- data/lua-hooks/ext/lua/lua_dll.rc +26 -0
- data/lua-hooks/ext/lua/luac.c +200 -0
- data/lua-hooks/ext/lua/luac.rc +1 -0
- data/lua-hooks/ext/lua/luaconf.h +763 -0
- data/lua-hooks/ext/lua/luaconf.h.in +724 -0
- data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
- data/lua-hooks/ext/lua/lualib.h +53 -0
- data/lua-hooks/ext/lua/lundump.c +227 -0
- data/lua-hooks/ext/lua/lundump.h +36 -0
- data/lua-hooks/ext/lua/lvm.c +767 -0
- data/lua-hooks/ext/lua/lvm.h +36 -0
- data/lua-hooks/ext/lua/lzio.c +82 -0
- data/lua-hooks/ext/lua/lzio.h +67 -0
- data/lua-hooks/ext/lua/print.c +227 -0
- data/lua-hooks/ext/luautf8/README.md +152 -0
- data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
- data/lua-hooks/ext/luautf8/unidata.h +3064 -0
- data/lua-hooks/lib/boot.lua +254 -0
- data/lua-hooks/lib/encode.lua +4 -0
- data/lua-hooks/lib/lexers/LICENSE +21 -0
- data/lua-hooks/lib/lexers/bash.lua +134 -0
- data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
- data/lua-hooks/lib/lexers/css.lua +216 -0
- data/lua-hooks/lib/lexers/html.lua +106 -0
- data/lua-hooks/lib/lexers/javascript.lua +68 -0
- data/lua-hooks/lib/lexers/lexer.lua +1575 -0
- data/lua-hooks/lib/lexers/markers.lua +33 -0
- metadata +308 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
module Immunio
|
2
|
+
# API for triggering the Immunio authentication hooks.
|
3
|
+
module Authentication
|
4
|
+
# Each method below takes an options hash argument. The options hash may
|
5
|
+
# contain any of the following information:
|
6
|
+
#
|
7
|
+
# * user_id: String or Number
|
8
|
+
# * username: String
|
9
|
+
# * email: String
|
10
|
+
# * user_record: ActiveRecord object for the user
|
11
|
+
# * reason: String (for failures)
|
12
|
+
|
13
|
+
# Call after a successful login.
|
14
|
+
def login(options_ro={})
|
15
|
+
plugin, options = parse_opts(options_ro)
|
16
|
+
return unless plugin
|
17
|
+
Immunio.run_hook! plugin, "authenticate", options.merge({is_valid: true})
|
18
|
+
Immunio.run_hook! plugin, "framework_login", options
|
19
|
+
end
|
20
|
+
|
21
|
+
# Call after an unsuccessful login.
|
22
|
+
def failed_login(options_ro={})
|
23
|
+
plugin, options = parse_opts(options_ro)
|
24
|
+
return unless plugin
|
25
|
+
Immunio.run_hook! plugin, "authenticate", options.merge({is_valid: false})
|
26
|
+
end
|
27
|
+
|
28
|
+
# Call after a user logs out.
|
29
|
+
def logout(options_ro={})
|
30
|
+
plugin, options = parse_opts(options_ro)
|
31
|
+
return unless plugin
|
32
|
+
Immunio.run_hook! plugin, "framework_logout", options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Call after the current user is changed.
|
36
|
+
def set_user(options_ro={})
|
37
|
+
plugin, options = parse_opts(options_ro)
|
38
|
+
return unless plugin
|
39
|
+
Immunio.run_hook! plugin, "framework_user", options
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call after a successful password reset has been requested for a user.
|
43
|
+
def password_reset(options_ro={})
|
44
|
+
plugin, options = parse_opts(options_ro)
|
45
|
+
return unless plugin
|
46
|
+
options[:is_valid] = true
|
47
|
+
Immunio.run_hook! plugin, "framework_password_reset", options
|
48
|
+
end
|
49
|
+
|
50
|
+
# Call after a failed password reset has been requested.
|
51
|
+
def failed_password_reset(options_ro={})
|
52
|
+
plugin, options = parse_opts(options_ro)
|
53
|
+
return unless plugin
|
54
|
+
options[:is_valid] = false
|
55
|
+
Immunio.run_hook! plugin, "framework_password_reset", options
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def parse_opts(options_ro)
|
60
|
+
unless options_ro.is_a? Hash
|
61
|
+
Immunio.logger.warn "Passed a non-hash options object into an authentication method: #{options_ro.inspect}"
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
Immunio.logger.debug {"Authentication API called with options #{options_ro}"}
|
66
|
+
|
67
|
+
options = options_ro.clone
|
68
|
+
plugin = options.delete(:plugin) { "api" }
|
69
|
+
options[:user_id] = options[:user_id].to_s if options[:user_id]
|
70
|
+
|
71
|
+
user_record = options.delete :user_record
|
72
|
+
if user_record
|
73
|
+
# Prefer user info passed in over those parsed from user_record
|
74
|
+
record_info = parse_record(user_record)
|
75
|
+
options = record_info.merge(options)
|
76
|
+
end
|
77
|
+
|
78
|
+
return plugin, options
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_record(record)
|
82
|
+
info = {}
|
83
|
+
|
84
|
+
info[:user_id] = record.id.to_s if record.respond_to?(:id)
|
85
|
+
info[:email] = record.email.to_s if record.respond_to?(:email)
|
86
|
+
|
87
|
+
attribute = [:username, :login, :name].detect { |attr| record.respond_to?(attr) }
|
88
|
+
info[:username] = record.send(attribute).to_s if attribute
|
89
|
+
|
90
|
+
info
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Make all module methods accessible from `Immunio` module. Eg.: `Immunio.login`
|
95
|
+
extend Authentication
|
96
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Immunio
|
2
|
+
# Rack app called when a request is blocked.
|
3
|
+
# Change this to customize the response sent on blocked requests.
|
4
|
+
# Defaults to a plain text 403 response.
|
5
|
+
mattr_accessor :blocked_app
|
6
|
+
mattr_accessor :override_response
|
7
|
+
|
8
|
+
self.blocked_app = ->(_env) do
|
9
|
+
[
|
10
|
+
403,
|
11
|
+
{ 'Content-Type' => 'text/plain' },
|
12
|
+
["Request blocked"]
|
13
|
+
]
|
14
|
+
end
|
15
|
+
|
16
|
+
self.override_response = ->(_env, override) do
|
17
|
+
[
|
18
|
+
override.status,
|
19
|
+
list_to_headers(override.headers.to_a),
|
20
|
+
[override.body]
|
21
|
+
]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.list_to_headers(list)
|
25
|
+
new_headers = {}
|
26
|
+
list.each do |name, value|
|
27
|
+
# If this header is already in `new_headers`, append to the
|
28
|
+
# existing value with a linefeed separator.
|
29
|
+
if new_headers.has_key?(name)
|
30
|
+
new_headers[name] += ("\n" + value)
|
31
|
+
else
|
32
|
+
new_headers[name] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
new_headers
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,432 @@
|
|
1
|
+
require "thread"
|
2
|
+
require "faraday"
|
3
|
+
require "faraday_middleware"
|
4
|
+
require "msgpack"
|
5
|
+
require "base64"
|
6
|
+
|
7
|
+
require_relative "errors"
|
8
|
+
require_relative "logger"
|
9
|
+
require_relative "version"
|
10
|
+
|
11
|
+
module Immunio
|
12
|
+
# Communication channel with the Immunio webservice.
|
13
|
+
class Channel
|
14
|
+
DIGEST = OpenSSL::Digest.new('sha1')
|
15
|
+
|
16
|
+
attr_reader :success_count, :error_count, :message_queue
|
17
|
+
attr_reader :rejected_message_count
|
18
|
+
|
19
|
+
def initialize(config)
|
20
|
+
@config = config
|
21
|
+
|
22
|
+
@agent_uuid = nil
|
23
|
+
|
24
|
+
# Messages waiting to be sent.
|
25
|
+
@message_queue = Queue.new
|
26
|
+
# Messages that were sent but failed. Need to be resent.
|
27
|
+
@send_buffer = []
|
28
|
+
@send_buffer_bytes = 0
|
29
|
+
@last_report_time = 0
|
30
|
+
|
31
|
+
# A large message we may have popped from the queue but couldn't fit
|
32
|
+
# in the current report
|
33
|
+
@next_message = nil
|
34
|
+
|
35
|
+
@send_seq = 0
|
36
|
+
@dropped_message_count = 0
|
37
|
+
@rejected_message_count = 0
|
38
|
+
@success_count = 0
|
39
|
+
@error_count = 0
|
40
|
+
@quick_connect = true
|
41
|
+
|
42
|
+
@started = false
|
43
|
+
@ready = false
|
44
|
+
|
45
|
+
@callbacks = []
|
46
|
+
|
47
|
+
# Anything looking to add to the messages sent to the server:
|
48
|
+
@senders = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def ready?
|
52
|
+
@ready
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_ready
|
56
|
+
@ready = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def started?
|
60
|
+
@started
|
61
|
+
end
|
62
|
+
|
63
|
+
def messages_count
|
64
|
+
@message_queue.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def start
|
68
|
+
return if @started
|
69
|
+
|
70
|
+
@started = true
|
71
|
+
@thread = Thread.new { run }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Stop and wait for the last messages to be sent.
|
75
|
+
def stop
|
76
|
+
return unless @started
|
77
|
+
|
78
|
+
Immunio.logger.debug "Stopping channel"
|
79
|
+
|
80
|
+
@started = false
|
81
|
+
@ready = false
|
82
|
+
|
83
|
+
if @thread
|
84
|
+
@thread.kill
|
85
|
+
@thread.join
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_message(message)
|
90
|
+
send_encoded_message message.to_msgpack
|
91
|
+
end
|
92
|
+
|
93
|
+
def send_encoded_message(message)
|
94
|
+
if @message_queue.size > @config.max_send_queue_size
|
95
|
+
Immunio.logger.warn "Dropping message for agent manager due to queue overflow (#{@message_queue.size} > #{@config.max_send_queue_size})"
|
96
|
+
Immunio.logger.debug "Dropped message: (#{message})"
|
97
|
+
# No room for this message on the queue. Discard.
|
98
|
+
@dropped_message_count += 1
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
Immunio.logger.debug {"Sending message to backend: #{MessagePack.unpack(message)}"}
|
103
|
+
|
104
|
+
@message_queue << message
|
105
|
+
end
|
106
|
+
|
107
|
+
def on_message(&block)
|
108
|
+
@callbacks << block
|
109
|
+
end
|
110
|
+
|
111
|
+
def on_sending(&block)
|
112
|
+
@senders << block
|
113
|
+
end
|
114
|
+
|
115
|
+
# Wait until we receive a message from the agentmanager.
|
116
|
+
# This is used primarily for internal testing to wait until all the hooks
|
117
|
+
# are loaded.
|
118
|
+
def wait_until_ready!
|
119
|
+
return if @ready
|
120
|
+
|
121
|
+
if @config.ready_timeout.to_i <= 0
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
Immunio.logger.debug "Channel waiting #{@config.ready_timeout.to_i} seconds until ready..."
|
126
|
+
Timeout.timeout @config.ready_timeout.to_i do
|
127
|
+
# Wait until we get a response from the agentmanager
|
128
|
+
sleep 0.1 until ready?
|
129
|
+
Immunio.logger.debug "Channel ready!"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
protected
|
134
|
+
def setup_connection(faraday)
|
135
|
+
# Override to modify Faraday options & middlewares
|
136
|
+
faraday.adapter Faraday.default_adapter
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
# Core method running in a thread
|
141
|
+
def run
|
142
|
+
Immunio.logger.debug "Starting channel on thread #{Thread.current.object_id}"
|
143
|
+
# Create an empty cert_store to prevent Faraday from using the system default OpenSSL store.
|
144
|
+
cert_store = OpenSSL::X509::Store.new
|
145
|
+
# Setup the connection for making requests to the server.
|
146
|
+
@connection = Faraday::Connection.new(@config.hello_url, ssl: { ca_file: "#{Immunio::DIR}/immunio/immunio_ca.crt", cert_store: cert_store },
|
147
|
+
request: { timeout: @config.http_timeout }) do |faraday|
|
148
|
+
faraday.request :url_encoded
|
149
|
+
|
150
|
+
# Provide a hook for additional Faraday config.
|
151
|
+
setup_connection faraday
|
152
|
+
end
|
153
|
+
|
154
|
+
# Get the polling URL from the server.
|
155
|
+
hello
|
156
|
+
|
157
|
+
# Start sending messages
|
158
|
+
while @started
|
159
|
+
poll
|
160
|
+
@success_count += 1
|
161
|
+
@error_count = 0
|
162
|
+
end
|
163
|
+
|
164
|
+
rescue StandardError => e
|
165
|
+
# Retry forever on error
|
166
|
+
@error_count += 1
|
167
|
+
|
168
|
+
log_error(e)
|
169
|
+
|
170
|
+
@success_count = 0
|
171
|
+
@quick_connect = true
|
172
|
+
|
173
|
+
exponential_backoff
|
174
|
+
|
175
|
+
retry
|
176
|
+
end
|
177
|
+
|
178
|
+
def log_error(e)
|
179
|
+
if @error_count == 1
|
180
|
+
Immunio.logger.warn "Connection failed after #{@success_count} successes: #{e} (#{e.class})"
|
181
|
+
else
|
182
|
+
Immunio.logger.warn "Connection failure [#{@error_count}]: #{e} (#{e.class})"
|
183
|
+
end
|
184
|
+
Immunio.logger.debug e.backtrace.join("\n")
|
185
|
+
end
|
186
|
+
|
187
|
+
def exponential_backoff()
|
188
|
+
# Exponential backoff
|
189
|
+
delay_ms = @config.initial_delay_ms * (2 ** (@error_count - 1))
|
190
|
+
# Cap at max_delay_ms
|
191
|
+
delay_ms = [delay_ms, @config.max_delay_ms].min
|
192
|
+
|
193
|
+
# choose a random delay less than the computed delay.
|
194
|
+
# The randomness avoids a herd effect from transient failures.
|
195
|
+
delay_ms *= rand
|
196
|
+
delay_ms = delay_ms.round
|
197
|
+
|
198
|
+
Immunio.logger.info "Delaying #{delay_ms} ms before next request"
|
199
|
+
sleep delay_ms / 1000.0
|
200
|
+
end
|
201
|
+
|
202
|
+
def notify(message)
|
203
|
+
message = message.symbolize_keys!
|
204
|
+
@callbacks.each { |callback| callback.call(message) }
|
205
|
+
end
|
206
|
+
|
207
|
+
def raw_log(raw)
|
208
|
+
raw.encode 'utf-8', invalid: :replace, undef: :replace
|
209
|
+
end
|
210
|
+
|
211
|
+
# Send initial hello request.
|
212
|
+
def hello
|
213
|
+
response = @connection.get do |req|
|
214
|
+
req.url "/"
|
215
|
+
|
216
|
+
req.params['name'] = AGENT_TYPE
|
217
|
+
req.params['version'] = VERSION
|
218
|
+
req.params['key'] = @config[:key]
|
219
|
+
|
220
|
+
req.headers['Accept'] = 'application/x-msgpack'
|
221
|
+
|
222
|
+
Immunio.logger.trace {"Sending hello request to agent manager (#{req})"}
|
223
|
+
end
|
224
|
+
|
225
|
+
if response.status != 200 then
|
226
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{raw_log response.body}"
|
227
|
+
end
|
228
|
+
|
229
|
+
Immunio.logger.trace {"Received hello response from agent manager (status: #{response.status}, body: #{raw_log response.body})"}
|
230
|
+
|
231
|
+
body = MessagePack.unpack(response.body)
|
232
|
+
|
233
|
+
@polling_url = body["url"]
|
234
|
+
|
235
|
+
if @polling_url.blank? then
|
236
|
+
raise Error, "No URL in HELLO response: #{response.body}"
|
237
|
+
end
|
238
|
+
|
239
|
+
Immunio.logger.info "Agent connected to #{@config.hello_url}"
|
240
|
+
end
|
241
|
+
|
242
|
+
# Execute a block for at max a given time to match `@config.max_report_interval`.
|
243
|
+
def with_report_timeout
|
244
|
+
time_since_last_report = Time.now.to_i - @last_report_time
|
245
|
+
|
246
|
+
# Interval expired already
|
247
|
+
return if time_since_last_report >= @config.max_report_interval
|
248
|
+
|
249
|
+
# Compute time to wait for new messages
|
250
|
+
timeout = @config.max_report_interval - time_since_last_report
|
251
|
+
|
252
|
+
Timeout.timeout timeout do
|
253
|
+
yield
|
254
|
+
end
|
255
|
+
|
256
|
+
rescue Timeout::Error # rubocop:disable Lint/HandleExceptions
|
257
|
+
# Timeout expired
|
258
|
+
end
|
259
|
+
|
260
|
+
def send_buffer_has_room(extra_bytes)
|
261
|
+
used_bytes = @send_buffer_bytes + extra_bytes + @next_message.bytesize
|
262
|
+
return @send_buffer.size < @config.max_report_size && used_bytes < @config.max_report_bytes
|
263
|
+
end
|
264
|
+
|
265
|
+
def add_to_send_buffer(message)
|
266
|
+
@send_buffer_bytes += message.bytesize
|
267
|
+
@send_buffer << message
|
268
|
+
end
|
269
|
+
|
270
|
+
# Fill send_buffer with messages to send
|
271
|
+
def collect_messages(used_bytes)
|
272
|
+
# if we had a message leftover from last send, make sure its not
|
273
|
+
# so big that it would fill the whole buffer
|
274
|
+
if @next_message != nil
|
275
|
+
if send_buffer_has_room used_bytes
|
276
|
+
add_to_send_buffer @next_message
|
277
|
+
else
|
278
|
+
Immunio.logger.warn "Dropped message over max byte send size, next message size #{@next_message.bytesize}"
|
279
|
+
Immunio.logger.debug "Dropped next message used: #{used_bytes} over max byte: #{@next_message}"
|
280
|
+
@dropped_message_count += 1
|
281
|
+
end
|
282
|
+
@next_message = nil
|
283
|
+
end
|
284
|
+
|
285
|
+
# Empty the queue as much as possible.
|
286
|
+
while !@message_queue.empty?
|
287
|
+
@next_message = @message_queue.pop
|
288
|
+
if !send_buffer_has_room used_bytes
|
289
|
+
break
|
290
|
+
end
|
291
|
+
|
292
|
+
add_to_send_buffer @next_message
|
293
|
+
@next_message = nil
|
294
|
+
end
|
295
|
+
|
296
|
+
# Return immediately if we've got the minimum number of messages to
|
297
|
+
# report or we're still waiting for an agent.ready message,
|
298
|
+
# or if we're at the maximum buffer size
|
299
|
+
return @send_buffer if @send_buffer.size >= @config.min_report_size || @next_message != nil
|
300
|
+
|
301
|
+
# Wait for messages from the queue until we have enough or get a timeout.
|
302
|
+
with_report_timeout do
|
303
|
+
while @send_buffer.size < @config.min_report_size
|
304
|
+
# If there are no messages in the queue, this will block until one arrives.
|
305
|
+
@next_message = @message_queue.pop
|
306
|
+
if !send_buffer_has_room used_bytes
|
307
|
+
break
|
308
|
+
end
|
309
|
+
|
310
|
+
add_to_send_buffer @next_message
|
311
|
+
@next_message = nil
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
@send_buffer
|
316
|
+
end
|
317
|
+
|
318
|
+
def gzip(s)
|
319
|
+
gzip_io = StringIO.new
|
320
|
+
begin
|
321
|
+
gzip = Zlib::GzipWriter.new(gzip_io)
|
322
|
+
gzip.write s
|
323
|
+
ret = gzip_io.string
|
324
|
+
ensure
|
325
|
+
gzip.close
|
326
|
+
end
|
327
|
+
ret
|
328
|
+
end
|
329
|
+
|
330
|
+
# Poll the server sending queued messages at the same time.
|
331
|
+
def poll
|
332
|
+
# Prep data
|
333
|
+
body = {
|
334
|
+
send_seq: @send_seq,
|
335
|
+
dropped_message_count: @dropped_message_count,
|
336
|
+
rejected_message_count: @rejected_message_count,
|
337
|
+
name: AGENT_TYPE,
|
338
|
+
version: VERSION,
|
339
|
+
vm_version: VM_VERSION,
|
340
|
+
}
|
341
|
+
|
342
|
+
body[:agent_uuid] = @agent_uuid if @agent_uuid
|
343
|
+
|
344
|
+
# Add any values that senders would like to send:
|
345
|
+
@senders.each { |callback|
|
346
|
+
body.merge! callback.call()
|
347
|
+
}
|
348
|
+
|
349
|
+
# Encode the body of the request using msgpack streaming API.
|
350
|
+
# See http://ruby.msgpack.org/MessagePack/Packer.html#write_map_header-instance_method for other methods.
|
351
|
+
packer = MessagePack::Packer.new
|
352
|
+
|
353
|
+
# Pack the `body` hash.
|
354
|
+
packer.write_map_header body.size + 1 # body's + 'msgs'
|
355
|
+
body.each_pair { |key, value| packer.write(key).write(value) }
|
356
|
+
|
357
|
+
packer.write 'msgs' # The key
|
358
|
+
if not @quick_connect
|
359
|
+
# Append the prepacked messages.
|
360
|
+
collect_messages packer.size
|
361
|
+
packer.write_array_header @send_buffer.size
|
362
|
+
@send_buffer.each { |message| packer.buffer << message }
|
363
|
+
else
|
364
|
+
packer.write_array_header 0
|
365
|
+
@quick_connect = false
|
366
|
+
end
|
367
|
+
|
368
|
+
encoded_body = packer.to_s
|
369
|
+
|
370
|
+
# Send the request
|
371
|
+
response = @connection.post do |req|
|
372
|
+
req.url @polling_url
|
373
|
+
|
374
|
+
req.params['name'] = AGENT_TYPE
|
375
|
+
req.params['version'] = VERSION
|
376
|
+
req.params['key'] = @config[:key]
|
377
|
+
req.params['sig'] = OpenSSL::HMAC.hexdigest(DIGEST, @config.secret, encoded_body)
|
378
|
+
|
379
|
+
req.headers['Content-Type'] = 'application/x-msgpack'
|
380
|
+
req.headers['Content-Encoding'] = 'gzip'
|
381
|
+
req.headers['Accept'] = 'application/x-msgpack'
|
382
|
+
|
383
|
+
req.body = gzip(encoded_body)
|
384
|
+
|
385
|
+
Immunio.logger.trace {"Sending request to agent manager (data: #{MessagePack.unpack(encoded_body)}, request: #{req})"}
|
386
|
+
end
|
387
|
+
|
388
|
+
if response.status >= 400 and response.status < 500 then
|
389
|
+
# 4XX response codes should NOT be retried. Discard the report.
|
390
|
+
@rejected_message_count += @send_buffer.size
|
391
|
+
Immunio.logger.trace "Rejecting #{@send_buffer.size} messages"
|
392
|
+
@send_buffer = []
|
393
|
+
@send_buffer_bytes = 0
|
394
|
+
|
395
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, raw body: #{raw_log response.body})"}
|
396
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{response.body}"
|
397
|
+
end
|
398
|
+
|
399
|
+
if response.status >= 500 then
|
400
|
+
# 5XX response codes are treated like errors.
|
401
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, raw body: #{raw_log response.body})"}
|
402
|
+
raise Error, "Bad response from Immunio server: #{response.status} #{response.body}"
|
403
|
+
end
|
404
|
+
|
405
|
+
body = MessagePack.unpack(response.body)
|
406
|
+
|
407
|
+
Immunio.logger.trace {"Received response from agent manager (status: #{response.status}, body: #{body}, raw body: #{raw_log response.body})"}
|
408
|
+
|
409
|
+
# Update local data from response
|
410
|
+
new_agent_uuid = body["agent_uuid"]
|
411
|
+
if new_agent_uuid
|
412
|
+
Immunio.logger.info "Agent UUID: #{new_agent_uuid}" if new_agent_uuid != @agent_uuid
|
413
|
+
@agent_uuid = new_agent_uuid
|
414
|
+
end
|
415
|
+
@send_seq += @send_buffer.size
|
416
|
+
|
417
|
+
# If messages were delivered successfully, clear send buffer.
|
418
|
+
if response.success?
|
419
|
+
@send_buffer = []
|
420
|
+
@send_buffer_bytes = 0
|
421
|
+
end
|
422
|
+
|
423
|
+
@last_report_time = Time.now.to_i
|
424
|
+
|
425
|
+
received_messages = body["msgs"]
|
426
|
+
if received_messages
|
427
|
+
received_messages.each { |message| notify message }
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|