scout 5.3.2 → 5.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +6 -0
- data/lib/scout.rb +1 -1
- data/lib/scout/command.rb +2 -0
- data/lib/scout/command/sign.rb +96 -0
- data/lib/scout/plugin_options.rb +12 -2
- data/lib/scout/server.rb +122 -44
- metadata +7 -6
data/CHANGELOG
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
== 5.3.3
|
2
|
+
|
3
|
+
* Sending embedded options to server for local & plugin overrides.
|
4
|
+
* Reading options for local plugins
|
5
|
+
* Added support for an account-specific public key (scout_rsa.pub)
|
6
|
+
|
1
7
|
== 5.3.2
|
2
8
|
|
3
9
|
* New --name="My Server" option to specify server name from the scout command.
|
data/lib/scout.rb
CHANGED
data/lib/scout/command.rb
CHANGED
@@ -44,6 +44,8 @@ module Scout
|
|
44
44
|
"PATH_TO_PLUGIN [PLUGIN_OPTIONS]"
|
45
45
|
opts.separator "[PLUGIN_OPTIONS] format: opt1=val1 opt2=val2 opt2=val3 ..."
|
46
46
|
opts.separator "Plugin will use internal defaults if options aren't provided."
|
47
|
+
opts.separator " Sign Code:"
|
48
|
+
opts.separator " #{program_name} [OPTIONS] sign PATH_TO_PLUGIN"
|
47
49
|
opts.separator " "
|
48
50
|
opts.separator "Note: This client is meant to be installed and"
|
49
51
|
opts.separator "invoked through cron or any other scheduler."
|
@@ -0,0 +1,96 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
require "pp"
|
4
|
+
require "openssl"
|
5
|
+
module Scout
|
6
|
+
class Command
|
7
|
+
class Sign < Command
|
8
|
+
HELP_URL = "https://scoutapp.com/info/creating_a_plugin#private_plugins"
|
9
|
+
CA_FILE = File.join( File.dirname(__FILE__),
|
10
|
+
*%w[.. .. .. data cacert.pem] )
|
11
|
+
VERIFY_MODE = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
12
|
+
|
13
|
+
def run
|
14
|
+
url, *provided_options = @args
|
15
|
+
# read the plugin_code from the file specified
|
16
|
+
if url.nil? or url == ''
|
17
|
+
puts "Please specify the path to the plugin (scout sign /path/to/plugin.rb)"
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
code=fetch_code(url)
|
22
|
+
if code.nil?
|
23
|
+
return
|
24
|
+
end
|
25
|
+
|
26
|
+
private_key = load_private_key
|
27
|
+
if private_key.nil?
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
puts "Signing code..."
|
32
|
+
code=code.gsub(/ +$/,'')
|
33
|
+
code_signature = private_key.sign( OpenSSL::Digest::SHA1.new, code)
|
34
|
+
sig=Base64.encode64(code_signature)
|
35
|
+
|
36
|
+
puts "Posting Signature..."
|
37
|
+
uri = URI.parse(url)
|
38
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
39
|
+
if uri.is_a?(URI::HTTPS)
|
40
|
+
http.use_ssl = true
|
41
|
+
http.ca_file = CA_FILE
|
42
|
+
http.verify_mode = VERIFY_MODE
|
43
|
+
end
|
44
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
45
|
+
request.set_form_data({'signature' => sig})
|
46
|
+
res = http.request(request)
|
47
|
+
if !res.is_a?(Net::HTTPOK)
|
48
|
+
puts "ERROR - Unable to post signature"
|
49
|
+
return
|
50
|
+
end
|
51
|
+
puts "...Success!"
|
52
|
+
rescue Timeout::Error
|
53
|
+
puts "ERROR - Unable to sign code (Timeout)"
|
54
|
+
rescue
|
55
|
+
puts "ERROR - Unable to sign code:"
|
56
|
+
puts $!
|
57
|
+
puts $!.backtrace
|
58
|
+
end # run
|
59
|
+
|
60
|
+
def fetch_code(url)
|
61
|
+
puts "Fetching code..."
|
62
|
+
uri = URI.parse(url)
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
64
|
+
if uri.is_a?(URI::HTTPS)
|
65
|
+
http.use_ssl = true
|
66
|
+
http.ca_file = CA_FILE
|
67
|
+
http.verify_mode = VERIFY_MODE
|
68
|
+
end
|
69
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
70
|
+
res = http.request(request)
|
71
|
+
if !res.is_a?(Net::HTTPOK)
|
72
|
+
puts "ERROR - Unable to fetch code: #{res.class}."
|
73
|
+
return
|
74
|
+
end
|
75
|
+
res.body
|
76
|
+
end
|
77
|
+
|
78
|
+
def load_private_key
|
79
|
+
private_key_path=File.expand_path("~/.scout/scout_rsa")
|
80
|
+
if !File.exist?(private_key_path)
|
81
|
+
puts "ERROR - Unable to find the private key at #{private_key_path} for code signing.\nSee #{HELP_URL} for help creating your account's key pair."
|
82
|
+
return nil
|
83
|
+
else
|
84
|
+
begin
|
85
|
+
OpenSSL::PKey::RSA.new(File.read(private_key_path))
|
86
|
+
rescue
|
87
|
+
puts "Error - Found a private key at #{private_key_path}, but unable to load the key:"
|
88
|
+
puts $!.message
|
89
|
+
puts "See #{HELP_URL} for help creating your account's key pair."
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end # load_private_key
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/scout/plugin_options.rb
CHANGED
@@ -5,7 +5,7 @@ require 'yaml'
|
|
5
5
|
module Scout
|
6
6
|
# a data structure of an individual plugin option
|
7
7
|
class PluginOption
|
8
|
-
attr_reader :name, :notes, :default, :advanced, :password, :required
|
8
|
+
attr_reader :name, :notes, :default, :advanced, :password, :required, :hash
|
9
9
|
def initialize(name, h)
|
10
10
|
@name=name
|
11
11
|
@notes=h['notes'] || ''
|
@@ -14,6 +14,7 @@ module Scout
|
|
14
14
|
@advanced = @attributes.include?('advanced')
|
15
15
|
@password = @attributes.include?('password')
|
16
16
|
@required = @attributes.include?('required')
|
17
|
+
@hash = h
|
17
18
|
end
|
18
19
|
|
19
20
|
# convenience -- for nicer syntax
|
@@ -27,6 +28,10 @@ module Scout
|
|
27
28
|
default_string = default == '' ? '' : " Default: #{default}. "
|
28
29
|
"'#{name}'#{required_string}#{default_string}#{notes}"
|
29
30
|
end
|
31
|
+
|
32
|
+
def to_hash
|
33
|
+
@hash
|
34
|
+
end
|
30
35
|
end
|
31
36
|
|
32
37
|
# A collection of pluginOption
|
@@ -43,7 +48,7 @@ module Scout
|
|
43
48
|
# attributes: required advanced
|
44
49
|
class PluginOptions < Array
|
45
50
|
|
46
|
-
attr_accessor :error
|
51
|
+
attr_accessor :error, :hash
|
47
52
|
|
48
53
|
# Should be valid YAML, a hash of hashes ... if not, will be caught in the rescue below
|
49
54
|
def self.from_yaml(string)
|
@@ -57,6 +62,7 @@ module Scout
|
|
57
62
|
ensure
|
58
63
|
res=PluginOptions.new(options_array)
|
59
64
|
res.error=error
|
65
|
+
res.hash = items unless res.error
|
60
66
|
return res
|
61
67
|
end
|
62
68
|
|
@@ -75,6 +81,10 @@ module Scout
|
|
75
81
|
end
|
76
82
|
res.join("\n")
|
77
83
|
end
|
84
|
+
|
85
|
+
def to_hash
|
86
|
+
hash
|
87
|
+
end
|
78
88
|
|
79
89
|
end
|
80
90
|
end
|
data/lib/scout/server.rb
CHANGED
@@ -49,10 +49,12 @@ module Scout
|
|
49
49
|
@logger = logger
|
50
50
|
@server_name = server_name
|
51
51
|
@plugin_plan = []
|
52
|
+
@plugins_with_signature_errors = []
|
52
53
|
@directives = {} # take_snapshots, interval, sleep_interval
|
53
54
|
@new_plan = false
|
54
55
|
@local_plugin_path = File.dirname(history_file) # just put overrides and ad-hoc plugins in same directory as history file.
|
55
56
|
@plugin_config_path = File.join(@local_plugin_path, "plugins.properties")
|
57
|
+
@account_public_key_path = File.join(@local_plugin_path, "scout_rsa.pub")
|
56
58
|
@plugin_config = load_plugin_configs(@plugin_config_path)
|
57
59
|
|
58
60
|
# the block is only passed for install and test, since we split plan retrieval outside the lockfile for run
|
@@ -64,7 +66,7 @@ module Scout
|
|
64
66
|
end
|
65
67
|
|
66
68
|
def refresh?
|
67
|
-
return true if !ping_key
|
69
|
+
return true if !ping_key or account_public_key_changed? # fetch the plan again if the account key is modified/created
|
68
70
|
|
69
71
|
url=URI.join( @server.sub("https://","http://"), "/clients/#{ping_key}/ping.scout")
|
70
72
|
|
@@ -103,52 +105,53 @@ module Scout
|
|
103
105
|
if res["Content-Encoding"] == "gzip" and body and not body.empty?
|
104
106
|
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
105
107
|
end
|
106
|
-
|
108
|
+
|
107
109
|
body_as_hash = JSON.parse(body)
|
108
|
-
|
109
|
-
# Ensure all the plugins in the new plan are properly signed. Load the public key for this.
|
110
|
-
public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] ))
|
111
|
-
debug "Loaded public key used for verifying code signatures (#{public_key_text.size} bytes)"
|
112
|
-
code_public_key = OpenSSL::PKey::RSA.new(public_key_text)
|
113
|
-
|
110
|
+
|
114
111
|
temp_plugins=Array(body_as_hash["plugins"])
|
115
|
-
|
116
|
-
temp_plugins.each do |plugin|
|
112
|
+
temp_plugins.each_with_index do |plugin,i|
|
117
113
|
signature=plugin['signature']
|
118
114
|
id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
|
119
115
|
if signature
|
120
116
|
code=plugin['code'].gsub(/ +$/,'') # we strip trailing whitespace before calculating signatures. Same here.
|
121
117
|
decoded_signature=Base64.decode64(signature)
|
122
|
-
if !
|
123
|
-
|
124
|
-
|
118
|
+
if !scout_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
|
119
|
+
if account_public_key
|
120
|
+
if !account_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
|
121
|
+
info "#{id_and_name} signature verification failed for both the Scout and account public keys"
|
122
|
+
plugin['sig_error'] = "The code signature failed verification against both the Scout and account public key. Please ensure the public key installed at #{@account_public_key_path} was generated with the same private key used to sign the plugin."
|
123
|
+
@plugins_with_signature_errors << temp_plugins.delete_at(i)
|
124
|
+
end
|
125
|
+
else
|
126
|
+
info "#{id_and_name} signature doesn't match!"
|
127
|
+
plugin['sig_error'] = "The code signature failed verification. Please place your account-specific public key at #{@account_public_key_path}."
|
128
|
+
@plugins_with_signature_errors << temp_plugins.delete_at(i)
|
129
|
+
end
|
125
130
|
end
|
131
|
+
# filename is set for local plugins. these don't have signatures.
|
132
|
+
elsif plugin['filename']
|
133
|
+
plugin['code']=nil # should not have any code.
|
126
134
|
else
|
127
135
|
info "#{id_and_name} has no signature!"
|
128
|
-
|
136
|
+
plugin['sig_error'] = "The code has no signature and cannot be verified."
|
137
|
+
@plugins_with_signature_errors << temp_plugins.delete_at(i)
|
129
138
|
end
|
130
139
|
end
|
131
140
|
|
141
|
+
@plugin_plan = temp_plugins
|
142
|
+
@directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
|
143
|
+
@history["plan_last_modified"] = res["last-modified"]
|
144
|
+
@history["old_plugins"] = @plugin_plan
|
145
|
+
@history["directives"] = @directives
|
132
146
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
@history["plan_last_modified"] = res["last-modified"]
|
137
|
-
@history["old_plugins"] = @plugin_plan.clone # important that the plan is cloned -- we're going to add local plugins, and they shouldn't go into history
|
138
|
-
@history["directives"] = @directives
|
147
|
+
info "Plan loaded. (#{@plugin_plan.size} plugins: " +
|
148
|
+
"#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
|
149
|
+
". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
|
139
150
|
|
140
|
-
|
141
|
-
"#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
|
142
|
-
". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
|
151
|
+
@new_plan = true # used in determination if we should checkin this time or not
|
143
152
|
|
144
|
-
@new_plan = true # used in determination if we should checkin this time or not
|
145
|
-
else
|
146
|
-
info "There was a problem with plugin signatures. Reusing old plan."
|
147
|
-
@plugin_plan = Array(@history["old_plugins"])
|
148
|
-
@directives = @history["directives"] || Hash.new
|
149
|
-
end
|
150
153
|
|
151
|
-
# Add local plugins to the plan.
|
154
|
+
# Add local plugins to the plan.
|
152
155
|
@plugin_plan += get_local_plugins
|
153
156
|
rescue Exception =>e
|
154
157
|
fatal "Plan from server was malformed: #{e.message} - #{e.backtrace}"
|
@@ -161,6 +164,7 @@ module Scout
|
|
161
164
|
@plugin_plan += get_local_plugins
|
162
165
|
@directives = @history["directives"] || Hash.new
|
163
166
|
end
|
167
|
+
@plugin_plan.reject! { |p| p['code'].nil? }
|
164
168
|
end
|
165
169
|
|
166
170
|
# returns an array of hashes representing local plugins found on the filesystem
|
@@ -169,13 +173,20 @@ module Scout
|
|
169
173
|
def get_local_plugins
|
170
174
|
local_plugin_paths=Dir.glob(File.join(@local_plugin_path,"[a-zA-Z]*.rb"))
|
171
175
|
local_plugin_paths.map do |plugin_path|
|
176
|
+
name = File.basename(plugin_path)
|
177
|
+
options = if directives = @plugin_plan.find { |plugin| plugin['filename'] == name }
|
178
|
+
directives['options']
|
179
|
+
else
|
180
|
+
nil
|
181
|
+
end
|
172
182
|
begin
|
173
183
|
{
|
174
|
-
'name'
|
175
|
-
'local_filename'
|
176
|
-
'origin'
|
177
|
-
'code'
|
178
|
-
'interval'
|
184
|
+
'name' => name,
|
185
|
+
'local_filename' => name,
|
186
|
+
'origin' => 'LOCAL',
|
187
|
+
'code' => File.read(plugin_path),
|
188
|
+
'interval' => 0,
|
189
|
+
'options' => options
|
179
190
|
}
|
180
191
|
rescue => e
|
181
192
|
info "Error trying to read local plugin: #{plugin_path} -- #{e.backtrace.join('\n')}"
|
@@ -193,6 +204,40 @@ module Scout
|
|
193
204
|
def ping_key
|
194
205
|
(@history['directives'] || {})['ping_key']
|
195
206
|
end
|
207
|
+
|
208
|
+
# Returns the Scout public key for code verification.
|
209
|
+
def scout_public_key
|
210
|
+
return @scout_public_key if instance_variables.include?('@scout_public_key')
|
211
|
+
public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_id_rsa.pub] ))
|
212
|
+
debug "Loaded scout-wide public key used for verifying code signatures (#{public_key_text.size} bytes)"
|
213
|
+
@scout_public_key = OpenSSL::PKey::RSA.new(public_key_text)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the account-specific public key if installed. Otherwise, nil.
|
217
|
+
def account_public_key
|
218
|
+
return @account_public_key if instance_variables.include?('@account_public_key')
|
219
|
+
@account_public_key = nil
|
220
|
+
begin
|
221
|
+
public_key_text = File.read(@account_public_key_path)
|
222
|
+
debug "Loaded account public key used for verifying code signatures (#{public_key_text.size} bytes)"
|
223
|
+
@account_public_key=OpenSSL::PKey::RSA.new(public_key_text)
|
224
|
+
rescue Errno::ENOENT
|
225
|
+
debug "No account private key provided"
|
226
|
+
rescue
|
227
|
+
info "Error loading account public key: #{$!.message}"
|
228
|
+
end
|
229
|
+
return @account_public_key
|
230
|
+
end
|
231
|
+
|
232
|
+
# This is called in +run_plugins_by_plan+. When the agent starts its next run, it checks to see
|
233
|
+
# if the key has changed. If so, it forces a refresh.
|
234
|
+
def store_account_public_key
|
235
|
+
@history['account_public_key'] = account_public_key.to_s
|
236
|
+
end
|
237
|
+
|
238
|
+
def account_public_key_changed?
|
239
|
+
@history['account_public_key'] != account_public_key.to_s
|
240
|
+
end
|
196
241
|
|
197
242
|
# uses values from history and current time to determine if we should checkin at this time
|
198
243
|
def time_to_checkin?
|
@@ -245,9 +290,20 @@ module Scout
|
|
245
290
|
end
|
246
291
|
end
|
247
292
|
take_snapshot if @directives['take_snapshots']
|
293
|
+
process_signature_errors
|
294
|
+
store_account_public_key
|
248
295
|
checkin
|
249
296
|
end
|
250
297
|
|
298
|
+
# Reports errors if there are any plugins with invalid signatures and sets a flag
|
299
|
+
# to force a fresh plan on the next run.
|
300
|
+
def process_signature_errors
|
301
|
+
return unless @plugins_with_signature_errors and @plugins_with_signature_errors.any?
|
302
|
+
@plugins_with_signature_errors.each do |plugin|
|
303
|
+
@checkin[:errors] << build_report(plugin,:subject => "Code Signature Error", :body => plugin['sig_error'])
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
251
307
|
#
|
252
308
|
# This is the heart of Scout.
|
253
309
|
#
|
@@ -292,7 +348,7 @@ module Scout
|
|
292
348
|
info "Plugin compiled."
|
293
349
|
rescue Exception
|
294
350
|
raise if $!.is_a? SystemExit
|
295
|
-
error "Plugin would not compile: #{$!.message}"
|
351
|
+
error "Plugin #{plugin['path'] || plugin['name']} would not compile: #{$!.message}"
|
296
352
|
@checkin[:errors] << build_report(plugin,:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}")
|
297
353
|
return
|
298
354
|
end
|
@@ -310,7 +366,6 @@ module Scout
|
|
310
366
|
end
|
311
367
|
end
|
312
368
|
|
313
|
-
|
314
369
|
debug "Loading plugin..."
|
315
370
|
if job = Plugin.last_defined.load( last_run, (memory || Hash.new), options)
|
316
371
|
info "Plugin loaded."
|
@@ -336,6 +391,7 @@ module Scout
|
|
336
391
|
:subject => "Plugin failed to run",
|
337
392
|
:body=>"#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}")
|
338
393
|
end
|
394
|
+
|
339
395
|
info "Plugin completed its run."
|
340
396
|
|
341
397
|
%w[report alert error summary].each do |type|
|
@@ -350,6 +406,8 @@ module Scout
|
|
350
406
|
end
|
351
407
|
end
|
352
408
|
|
409
|
+
report_embedded_options(plugin,code_to_run)
|
410
|
+
|
353
411
|
@history["last_runs"].delete(plugin['name'])
|
354
412
|
@history["memory"].delete(plugin['name'])
|
355
413
|
@history["last_runs"][id_and_name] = run_time
|
@@ -379,6 +437,22 @@ module Scout
|
|
379
437
|
end
|
380
438
|
info "Plugin '#{plugin['name']}' processing complete."
|
381
439
|
end
|
440
|
+
|
441
|
+
# Adds embedded options to the checkin if the plugin is manually installed
|
442
|
+
# on this server.
|
443
|
+
def report_embedded_options(plugin,code)
|
444
|
+
return unless plugin['origin'] and Plugin.has_embedded_options?(code)
|
445
|
+
if options_yaml = Plugin.extract_options_yaml_from_code(code)
|
446
|
+
options=PluginOptions.from_yaml(options_yaml)
|
447
|
+
if options.error
|
448
|
+
debug "Problem parsing option definition in the plugin code:"
|
449
|
+
debug options_yaml
|
450
|
+
else
|
451
|
+
debug "Sending options to server"
|
452
|
+
@checkin[:options] << build_report(plugin,options.to_hash)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
382
456
|
|
383
457
|
|
384
458
|
# captures a list of processes running at this moment
|
@@ -393,13 +467,14 @@ module Scout
|
|
393
467
|
|
394
468
|
# Prepares a check-in data structure to hold Plugin generated data.
|
395
469
|
def prepare_checkin
|
396
|
-
@checkin = { :reports
|
397
|
-
:alerts
|
398
|
-
:errors
|
399
|
-
:summaries
|
400
|
-
:snapshot
|
401
|
-
:config_path
|
402
|
-
:server_name
|
470
|
+
@checkin = { :reports => Array.new,
|
471
|
+
:alerts => Array.new,
|
472
|
+
:errors => Array.new,
|
473
|
+
:summaries => Array.new,
|
474
|
+
:snapshot => '',
|
475
|
+
:config_path => File.expand_path(File.dirname(@history_file)),
|
476
|
+
:server_name => @server_name,
|
477
|
+
:options => Array.new}
|
403
478
|
end
|
404
479
|
|
405
480
|
def show_checkin(printer = :p)
|
@@ -522,6 +597,9 @@ module Scout
|
|
522
597
|
end
|
523
598
|
|
524
599
|
def checkin
|
600
|
+
debug """
|
601
|
+
#{PP.pp(@checkin, '')}
|
602
|
+
"""
|
525
603
|
@history['last_checkin'] = Time.now.to_i # might have to save the time of invocation and use here to prevent drift
|
526
604
|
io = StringIO.new
|
527
605
|
gzip = Zlib::GzipWriter.new(io)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 61
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 5
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 5.3.
|
9
|
+
- 3
|
10
|
+
version: 5.3.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Scout Monitoring
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-06-28 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -51,6 +51,7 @@ extra_rdoc_files:
|
|
51
51
|
files:
|
52
52
|
- lib/scout/command/install.rb
|
53
53
|
- lib/scout/command/run.rb
|
54
|
+
- lib/scout/command/sign.rb
|
54
55
|
- lib/scout/command/test.rb
|
55
56
|
- lib/scout/command/troubleshoot.rb
|
56
57
|
- lib/scout/command.rb
|
@@ -221,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
222
|
requirements: []
|
222
223
|
|
223
224
|
rubyforge_project: scout
|
224
|
-
rubygems_version: 1.
|
225
|
+
rubygems_version: 1.4.2
|
225
226
|
signing_key:
|
226
227
|
specification_version: 3
|
227
228
|
summary: Scout makes monitoring and reporting on your web applications as flexible and simple as possible.
|