emonti-buby 1.0.0

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/lib/buby.rb ADDED
@@ -0,0 +1,462 @@
1
+ include Java
2
+
3
+ require 'pp'
4
+ require "buby.jar"
5
+
6
+ include_class 'BurpExtender'
7
+
8
+ # Buby is a mash-up of the commercial security testing web proxy PortSwigger
9
+ # Burp Suite(tm) allowing you to add scripting to Burp. Burp is driven from
10
+ # and tied to JRuby with a Java extension using the BurpExtender API.
11
+ #
12
+ # The Buby class is an abstract implementation of a BurpExtender ruby handler.
13
+ # Included are several abstract event handlers used from the BurpExtender
14
+ # java implementation:
15
+ # * evt_extender_init
16
+ # * evt_proxy_message
17
+ # * evt_command_line_args
18
+ # * evt_register_callbacks
19
+ # * evt_application_closing
20
+ #
21
+ # This class also exposes several methods used to access Burp functionality
22
+ # and user interfaces (note also, abbreviated aliases exist for each):
23
+ # * doActiveScan
24
+ # * doPassiveScan
25
+ # * excludeFromScope
26
+ # * includeInScope
27
+ # * isInScope
28
+ # * issueAlert
29
+ # * makeHttpRequest
30
+ # * sendToIntruder
31
+ # * sendToRepeater
32
+ # * sendToSpider
33
+ #
34
+ # Credit:
35
+ # * Burp and Burp Suite are trade-marks of PortSwigger Ltd.
36
+ # Copyright 2008 PortSwigger Ltd. All rights reserved.
37
+ # See http://portswigger.net for license terms.
38
+ #
39
+ # * This ruby library and the accompanying BurpExtender.java implementation
40
+ # were written by Eric Monti @ Matasano Security.
41
+ #
42
+ # Matasano claims no professional or legal affiliation with PortSwigger LTD.
43
+ # nor do we sell or officially endorse their products.
44
+ #
45
+ # However, this author would like to express his personal and professional
46
+ # respect and appreciation for their making available the IBurpExtender
47
+ # extension API. The availability of this interface in an already great tool
48
+ # goes a long way to make Burp Suite a truly first-class application.
49
+ #
50
+ # * Forgive the name. It won out over "Burb" and "BurpRub". It's just easier
51
+ # to type and say out-loud. Mike Tracy gets full credit as official
52
+ # Buby-namer.
53
+ #
54
+ class Buby
55
+
56
+ # :stopdoc:
57
+ VERSION = '1.0.0'
58
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
59
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
60
+ # :startdoc:
61
+
62
+ # Returns the internal reference to the BurpExtender instance. This
63
+ # reference gets set from Java through the evt_extender_init method.
64
+ def burp_extender; @burp_extender; end
65
+
66
+ # Returns the internal reference to the IBupExtenderCallbacks instance.
67
+ # This reference gets set from Java through the evt_register_callbacks
68
+ # method.
69
+ def burp_callbacks; @burp_callbacks; end
70
+
71
+ def _check_cb
72
+ @burp_callbacks or raise "Burp callbacks have not been set"
73
+ end
74
+
75
+ # Send an HTTP request to the Burp Scanner tool to perform an active
76
+ # vulnerability scan.
77
+ # * host = The hostname of the remote HTTP server.
78
+ # * port = The port of the remote HTTP server.
79
+ # * https = Flags whether the protocol is HTTPS or HTTP.
80
+ # * req = The full HTTP request.
81
+ def doActiveScan(host, port, https, req)
82
+ _check_cb.doActiveScan(host, port, https, req.to_java_bytes)
83
+ end
84
+
85
+ alias do_active_scan doActiveScan
86
+ alias active_scan doActiveScan
87
+
88
+ # Send an HTTP request and response to the Burp Scanner tool to perform a
89
+ # passive vulnerability scan.
90
+ # * host = The hostname of the remote HTTP server.
91
+ # * port = The port of the remote HTTP server.
92
+ # * https = Flags whether the protocol is HTTPS or HTTP.
93
+ # * req = The full HTTP request.
94
+ # * rsp = The full HTTP response.
95
+ def doPassiveScan(host, port, https, req, rsp)
96
+ _check_cb.doPassiveScan(host, port, https, req.to_java_bytes, rsp.to_java_bytes)
97
+ end
98
+
99
+ alias do_passive_scan doPassiveScan
100
+ alias passive_scan doPassiveScan
101
+
102
+ # Exclude the specified URL from the Suite-wide scope.
103
+ # * url = The URL to exclude from the Suite-wide scope.
104
+ def excludeFromScope(url)
105
+ _check_cb.excludeFromScope(java.net.URL.new(url.to_s))
106
+ end
107
+
108
+ alias exclude_from_scope excludeFromScope
109
+ alias exclude_scope excludeFromScope
110
+
111
+ # Include the specified URL in the Suite-wide scope.
112
+ # * url = The URL to exclude in the Suite-wide scope.
113
+ def includeInScope(url)
114
+ _check_cb.includeInScope(java.net.URL.new(url.to_s))
115
+ end
116
+
117
+ alias include_in_scope includeInScope
118
+ alias include_scope includeInScope
119
+
120
+ # Query whether a specified URL is within the current Suite-wide scope.
121
+ # * url = The URL to query
122
+ #
123
+ # Returns: true / false
124
+ def isInScope(url)
125
+ _check_cb.isInScope(java.net.URL.new(url.to_s))
126
+ end
127
+
128
+ alias is_in_scope isInScope
129
+ alias in_scope? isInScope
130
+
131
+ # Display a message in the Burp Suite alerts tab.
132
+ # * msg = The alert message to display.
133
+ def issueAlert(msg)
134
+ _check_cb.issueAlert(msg.to_s)
135
+ end
136
+
137
+ alias issue_alert issueAlert
138
+ alias alert issueAlert
139
+
140
+ # Issue an arbitrary HTTP request and retrieve its response
141
+ # * host = The hostname of the remote HTTP server.
142
+ # * port = The port of the remote HTTP server.
143
+ # * https = Flags whether the protocol is HTTPS or HTTP.
144
+ # * req = The full HTTP request.
145
+ #
146
+ # Returns: The full response retrieved from the remote server.
147
+ def makeHttpRequest(host, port, https, req)
148
+ String.from_java_bytes(
149
+ _check_cb.makeHttpRequest(host, port, https, req.to_java_bytes)
150
+ )
151
+ end
152
+
153
+ alias make_http_request makeHttpRequest
154
+ alias make_request makeHttpRequest
155
+
156
+ # Send an HTTP request to the Burp Intruder tool
157
+ # * host = The hostname of the remote HTTP server.
158
+ # * port = The port of the remote HTTP server.
159
+ # * https = Flags whether the protocol is HTTPS or HTTP.
160
+ # * req = The full HTTP request.
161
+ def sendToIntruder(host, port, https, req)
162
+ _check_cb.sendToIntruder(host, port, https, req.to_java_bytes)
163
+ end
164
+
165
+ alias send_to_intruder sendToIntruder
166
+ alias intruder sendToIntruder
167
+
168
+ # Send an HTTP request to the Burp Repeater tool.
169
+ # * host = The hostname of the remote HTTP server.
170
+ # * port = The port of the remote HTTP server.
171
+ # * https = Flags whether the protocol is HTTPS or HTTP.
172
+ # * req = The full HTTP request.
173
+ # * tab = The tab caption displayed in Repeater. (default: auto-generated)
174
+ def sendToRepeater(host, port, https, req, tab=nil)
175
+ _check_cb.sendToRepeater(host, port, https, req.to_java_bytes, tab)
176
+ end
177
+
178
+ alias send_to_repeater sendToRepeater
179
+ alias repeater sendToRepeater
180
+
181
+ # Send a seed URL to the Burp Spider tool.
182
+ # * url = The new seed URL to begin spidering from.
183
+ def sendToSpider(url)
184
+ _check_cb.includeInScope(java.net.URL.new(url.to_s))
185
+ end
186
+
187
+ alias send_to_spider sendToSpider
188
+ alias spider sendToSpider
189
+
190
+
191
+ ### Event Handlers ###
192
+
193
+ # This method is called by the BurpExtender java implementation upon
194
+ # initialization of the BurpExtender instance for Burp. The args parameter
195
+ # is passed with a instance of the newly initialized BurpExtender instance
196
+ # so that implementations can access and extend its public interfaces.
197
+ #
198
+ # The return value is ignored.
199
+ def evt_extender_init ext
200
+ @burp_extender = ext
201
+ pp([:got_extender, ext]) if $DEBUG
202
+ end
203
+
204
+ # This method is called by the BurpExtender implementation Burp startup.
205
+ # The args parameter contains main()'s argv command-line arguments array.
206
+ #
207
+ # Note: This maps to the 'setCommandLineArgs' method in the java
208
+ # implementation of BurpExtender.
209
+ #
210
+ # The return value is ignored.
211
+ def evt_command_line_args args
212
+ pp([:got_args, args]) if $DEBUG
213
+ end
214
+
215
+ # This method is called by BurpExtender on startup to register Burp's
216
+ # IBurpExtenderCallbacks interface object.
217
+ #
218
+ # This maps to the 'registerExtenderCallbacks' method in the Java
219
+ # implementation of BurpExtender.
220
+ #
221
+ # The return value is ignored.
222
+ def evt_register_callbacks cb
223
+ @burp_callbacks = cb
224
+ cb.issueAlert("[JRuby::#{self.class}] registered callback")
225
+ pp([:got_callbacks, cb]) if $DEBUG
226
+ end
227
+
228
+ ACTION_FOLLOW_RULES = BurpExtender::ACTION_FOLLOW_RULES
229
+ ACTION_DO_INTERCEPT = BurpExtender::ACTION_DO_INTERCEPT
230
+ ACTION_DONT_INTERCEPT = BurpExtender::ACTION_DONT_INTERCEPT
231
+ ACTION_DROP = BurpExtender::ACTION_DROP
232
+
233
+ # This method is called by BurpExtender while proxying HTTP messages and
234
+ # before passing them through the Burp proxy. Implementations can use this
235
+ # method to implement arbitrary processing upon HTTP requests and responses
236
+ # such as interception, logging, modification, and so on.
237
+ #
238
+ # The 'is_req' parameter indicates whether it is a response or request.
239
+ #
240
+ # Note: This method maps to the 'processProxyMessage' method in the java
241
+ # implementation of BurpExtender.
242
+ #
243
+ # Below are the parameters descriptions based on the IBurpExtender
244
+ # javadoc. Where applicable, decriptions have been modified for
245
+ # local parameter naming and other ruby-specific details added.
246
+ #
247
+ # * msg_ref:
248
+ # An identifier which is unique to a single request/response pair. This
249
+ # can be used to correlate details of requests and responses and perform
250
+ # processing on the response message accordingly. This number also
251
+ # corresponds to the Burp UI's proxy "history" # column.
252
+ #
253
+ # * is_req: (true/false)
254
+ # Flags whether the message is a client request or a server response.
255
+ #
256
+ # * rhost:
257
+ # The hostname of the remote HTTP server.
258
+ #
259
+ # * rport:
260
+ # The port of the remote HTTP server.
261
+ #
262
+ # * is_https:
263
+ # Flags whether the protocol is HTTPS or HTTP.
264
+ #
265
+ # * http_meth:
266
+ # The method verb used in the client request.
267
+ #
268
+ # * url:
269
+ # The requested URL. Set in both the request and response.
270
+ #
271
+ # * resourceType:
272
+ # The filetype of the requested resource, or a zero-length string if the
273
+ # resource has no filetype.
274
+ #
275
+ # * status:
276
+ # The HTTP status code returned by the server. This value is nil for
277
+ # request messages.
278
+ #
279
+ # * req_content_type:
280
+ # The content-type string returned by the server. This value is nil for
281
+ # request messages.
282
+ #
283
+ # * message:
284
+ # The full HTTP message.
285
+ # **Ruby note:
286
+ # For convenience, the message is received and returned as a ruby
287
+ # String object. Internally within Burp it is handled as a java byte[]
288
+ # array. See also the notes about the return object below.
289
+ #
290
+ # * action:
291
+ # An array containing a single integer, allowing the implementation to
292
+ # communicate back to Burp Proxy a non-default interception action for
293
+ # the message. The default value is ACTION_FOLLOW_RULES (or 0).
294
+ # Possible values include:
295
+ # ACTION_FOLLOW_RULES = 0
296
+ # ACTION_DO_INTERCEPT = 1
297
+ # ACTION_DONT_INTERCEPT = 2
298
+ # ACTION_DROP = 3
299
+ #
300
+ # Refer to the BurpExtender.java source comments for more details.
301
+ #
302
+ #
303
+ # Return Value:
304
+ # Implementations should return either (a) the same object received
305
+ # in the message paramater, or (b) a different object containing a
306
+ # modified message.
307
+ #
308
+ # **IMPORTANT RUBY NOTE:
309
+ # Always be sure to return a new object if making modifications to messages.
310
+ #
311
+ # Explanation:
312
+ # The (a) and (b) convention above is followed rather literally during type
313
+ # conversion on the return value back into the java BurpExtender.
314
+ #
315
+ # When determining whether a change has been made in the message or not,
316
+ # the decision is made based on whether the object returned is the same
317
+ # as the object submitted in the call to evt_proxy_message.
318
+ #
319
+ #
320
+ # So, for example, using in-place modification of the message using range
321
+ # substring assignments or destructive method variations like String.sub!()
322
+ # and String.gsub! alone won't work because the same object gets returned
323
+ # to BurpExtender.
324
+ #
325
+ # In short, this means that if you want modifications to be made, be sure
326
+ # to return a different String than the one you got in your handler.
327
+ #
328
+ # So for example this code won't do anything at all:
329
+ #
330
+ # ...
331
+ # message.sub!(/^GET /, "HEAD ")
332
+ # return message
333
+ #
334
+ # Nor this:
335
+ #
336
+ # message[0..4] = "HEAD "
337
+ # return message
338
+ #
339
+ # But this will
340
+ #
341
+ # ...
342
+ # return message.sub(/^GET /, "HEAD ")
343
+ #
344
+ # And so will this
345
+ #
346
+ # ...
347
+ # message[0..4] = "HEAD "
348
+ # return message.dup
349
+ #
350
+ def evt_proxy_message msg_ref, is_req, rhost, rport, is_https, http_meth, url, resourceType, status, req_content_type, message, action
351
+ pp([ (is_req)? :got_proxy_request : :got_proxy_response,
352
+ [:msg_ref, msg_ref],
353
+ [:is_req, is_req],
354
+ [:rhost, rhost],
355
+ [:rport, rport],
356
+ [:is_https, is_https],
357
+ [:http_meth, http_meth],
358
+ [:url, url],
359
+ [:resourceType, resourceType],
360
+ [:status, status],
361
+ [:req_content_type, req_content_type],
362
+ [:message, message],
363
+ [:action, action[0]] ]) if $DEBUG
364
+
365
+ return message
366
+ end
367
+
368
+ # This method is called by BurpExtender right before closing the
369
+ # application. Implementations can use this method to perform cleanup
370
+ # tasks such as closing files or databases before exit.
371
+ def evt_application_closing
372
+ pp([:got_app_close]) if $DEBUG
373
+ end
374
+
375
+ # Prepares the java BurpExtender implementation with a reference
376
+ # to self as the module handler and launches burp suite.
377
+ def start(args=[])
378
+ BurpExtender.set_handler(self)
379
+ Java::Burp::StartBurp.main(args.to_java(:string))
380
+ return self
381
+ end
382
+
383
+ # Starts burp using a supplied handler class,
384
+ # h_class = Buby or a derived class. instance of which will become handler.
385
+ # args = arguments to Burp
386
+ # init_args = arguments to the handler constructor
387
+ #
388
+ # Returns the handler instance
389
+ def self.start_burp(h_class=nil, init_args=nil, args=nil)
390
+ h_class ||= self
391
+ init_args ||= []
392
+ args ||= []
393
+ h_class.new(*init_args).start(args)
394
+ end
395
+
396
+ # Attempts to load burp with require and confirm it provides the required
397
+ # class in the Java namespace.
398
+ #
399
+ # Returns: true/false depending on whether the required jar provides us
400
+ # the required class
401
+ #
402
+ # Raises: may raise the usual require exceptions if jar_path is bad.
403
+ def self.load_burp(jar_path)
404
+ require jar_path
405
+ return burp_loaded?
406
+ end
407
+
408
+ # Checks the Java namespace to see if Burp has been loaded.
409
+ def self.burp_loaded?
410
+ begin
411
+ include_class 'burp.StartBurp'
412
+ return true
413
+ rescue
414
+ return false
415
+ end
416
+ end
417
+
418
+ ### Extra cruft added by Mr Bones:
419
+
420
+ # Returns the library path for the module. If any arguments are given,
421
+ # they will be joined to the end of the libray path using
422
+ # <tt>File.join</tt>.
423
+ #
424
+ def self.libpath( *args )
425
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
426
+ end
427
+
428
+ # Returns the lpath for the module. If any arguments are given,
429
+ # they will be joined to the end of the path using
430
+ # <tt>File.join</tt>.
431
+ #
432
+ def self.path( *args )
433
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
434
+ end
435
+
436
+ # Utility method used to require all files ending in .rb that lie in the
437
+ # directory below this file that has the same name as the filename passed
438
+ # in. Optionally, a specific _directory_ name can be passed in such that
439
+ # the _filename_ does not have to be equivalent to the directory.
440
+ #
441
+ def self.require_all_libs_relative_to( fname, dir = nil )
442
+ dir ||= ::File.basename(fname, '.*')
443
+ search_me = ::File.expand_path(
444
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
445
+
446
+ Dir.glob(search_me).sort.each {|rb| require rb}
447
+ end
448
+
449
+ # Returns the version string for the library.
450
+ #
451
+ def self.version
452
+ VERSION
453
+ end
454
+ end
455
+
456
+ # Try requiring 'burp.jar' from the Ruby lib-path
457
+ unless Buby.burp_loaded?
458
+ begin require "burp.jar"
459
+ rescue LoadError
460
+ end
461
+ end
462
+
data/samples/basic.rb ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env jruby
2
+ $: << File.join(File.dirname(__FILE__), %w[.. lib])
3
+
4
+ require 'buby'
5
+ $DEBUG = true
6
+ Buby.load_burp("/path/to/burp.jar") if not Buby.burp_loaded?
7
+ buby = Buby.start_burp()
8
+
9
+ require 'net/http'
10
+ p = Net::HTTP::Proxy("localhost", 8080).start("www.google.com")
11
+
12
+ # Note: I'm using 'instance_eval' here only to stay with the flow of the
13
+ # existing IRB session. Normally, you'd probably want to implement this as
14
+ # an override in your Buby-derived class.
15
+
16
+ buby.instance_eval do
17
+
18
+ def evt_proxy_message(*param)
19
+ msg_ref, is_req, rhost, rport, is_https, http_meth, url, resourceType,
20
+ status, req_content_type, message, action = param
21
+
22
+ if is_req and http_meth=="GET"
23
+ # Change the HTTP request verb to something silly
24
+ message[0,3] = "PET"
25
+
26
+ # Forcibly disable interception in the Burp UI
27
+ action[0] = Buby::ACTION_DONT_INTERCEPT
28
+
29
+ # Return a new instance and still get $DEBUG info
30
+ return super(*param).dup
31
+ else
32
+ # Just get $DEBUG info for all other requests
33
+ return super(*param)
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ # Now, make another request using the Net::HTTP client
40
+ p.get("/")
41
+
42
+
data/spec/buby_spec.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Buby do
5
+ end
6
+
7
+ # EOF
@@ -0,0 +1,16 @@
1
+
2
+ require File.expand_path(
3
+ File.join(File.dirname(__FILE__), %w[.. lib buby]))
4
+
5
+ Spec::Runner.configure do |config|
6
+ # == Mock Framework
7
+ #
8
+ # RSpec uses it's own mocking framework by default. If you prefer to
9
+ # use mocha, flexmock or RR, uncomment the appropriate line:
10
+ #
11
+ # config.mock_with :mocha
12
+ # config.mock_with :flexmock
13
+ # config.mock_with :rr
14
+ end
15
+
16
+ # EOF
data/tasks/ann.rake ADDED
@@ -0,0 +1,80 @@
1
+
2
+ begin
3
+ require 'bones/smtp_tls'
4
+ rescue LoadError
5
+ require 'net/smtp'
6
+ end
7
+ require 'time'
8
+
9
+ namespace :ann do
10
+
11
+ # A prerequisites task that all other tasks depend upon
12
+ task :prereqs
13
+
14
+ file PROJ.ann.file do
15
+ ann = PROJ.ann
16
+ puts "Generating #{ann.file}"
17
+ File.open(ann.file,'w') do |fd|
18
+ fd.puts("#{PROJ.name} version #{PROJ.version}")
19
+ fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
20
+ fd.puts(" #{PROJ.url}") if PROJ.url.valid?
21
+ fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
22
+ fd.puts
23
+ fd.puts("== DESCRIPTION")
24
+ fd.puts
25
+ fd.puts(PROJ.description)
26
+ fd.puts
27
+ fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
28
+ fd.puts
29
+ ann.paragraphs.each do |p|
30
+ fd.puts "== #{p.upcase}"
31
+ fd.puts
32
+ fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
33
+ fd.puts
34
+ end
35
+ fd.puts ann.text if ann.text
36
+ end
37
+ end
38
+
39
+ desc "Create an announcement file"
40
+ task :announcement => ['ann:prereqs', PROJ.ann.file]
41
+
42
+ desc "Send an email announcement"
43
+ task :email => ['ann:prereqs', PROJ.ann.file] do
44
+ ann = PROJ.ann
45
+ from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
46
+ to = Array(ann.email[:to])
47
+
48
+ ### build a mail header for RFC 822
49
+ rfc822msg = "From: #{from}\n"
50
+ rfc822msg << "To: #{to.join(',')}\n"
51
+ rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
52
+ rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
53
+ rfc822msg << "\n"
54
+ rfc822msg << "Date: #{Time.new.rfc822}\n"
55
+ rfc822msg << "Message-Id: "
56
+ rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
57
+ rfc822msg << File.read(ann.file)
58
+
59
+ params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
60
+ ann.email[key]
61
+ end
62
+
63
+ params[3] = PROJ.email if params[3].nil?
64
+
65
+ if params[4].nil?
66
+ STDOUT.write "Please enter your e-mail password (#{params[3]}): "
67
+ params[4] = STDIN.gets.chomp
68
+ end
69
+
70
+ ### send email
71
+ Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
72
+ end
73
+ end # namespace :ann
74
+
75
+ desc 'Alias to ann:announcement'
76
+ task :ann => 'ann:announcement'
77
+
78
+ CLOBBER << PROJ.ann.file
79
+
80
+ # EOF
data/tasks/bones.rake ADDED
@@ -0,0 +1,20 @@
1
+
2
+ if HAVE_BONES
3
+
4
+ namespace :bones do
5
+
6
+ desc 'Show the PROJ open struct'
7
+ task :debug do |t|
8
+ atr = if t.application.top_level_tasks.length == 2
9
+ t.application.top_level_tasks.pop
10
+ end
11
+
12
+ if atr then Bones::Debug.show_attr(PROJ, atr)
13
+ else Bones::Debug.show PROJ end
14
+ end
15
+
16
+ end # namespace :bones
17
+
18
+ end # HAVE_BONES
19
+
20
+ # EOF