scout 5.3.2 → 5.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|