defensio 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/.gitignore +2 -0
  2. data/LICENSE +21 -0
  3. data/README +0 -0
  4. data/Rakefile +26 -0
  5. data/VERSION +1 -0
  6. data/lib/defensio.rb +173 -0
  7. data/test/defensio_test.rb +288 -0
  8. metadata +71 -0
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ .DS_Store
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 Websense Inc.
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
13
+ all 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
21
+ THE SOFTWARE.
data/README ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = "defensio"
8
+ gemspec.summary = "Official Ruby library for Defensio 2.0"
9
+ gemspec.description = "Official Ruby library for Defensio 2.0"
10
+ gemspec.email = "support@defensio.com"
11
+ gemspec.homepage = "http://github.com/defensio/defensio-ruby"
12
+ gemspec.authors = ["Carl Mercier"]
13
+ gemspec.add_dependency('patron', '>= 0.4.4')
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
18
+ end
19
+
20
+ Rake::TestTask.new(:test) do |t|
21
+ t.libs << 'lib' << 'test'
22
+ t.pattern = 'test/**/*_test.rb'
23
+ t.verbose = false
24
+ end
25
+
26
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
data/lib/defensio.rb ADDED
@@ -0,0 +1,173 @@
1
+ #
2
+ # Defensio-Ruby version 0.1
3
+ # Written by the Defensio team at Websense, Inc.
4
+ #
5
+ # Make sure to install the necessary gems by running the following commands:
6
+ # $ sudo gem install patron -v 0.4.4
7
+ # On Linux, you'll also need to install the libcurl development package. On Ubuntu, simply do:
8
+ # $ sudo apt-get install libcurl4-openssl-dev
9
+ #
10
+
11
+ require 'rubygems'
12
+ require 'patron'
13
+ require 'uri'
14
+
15
+ class Defensio
16
+ # You shouldn't modify these values unless you really know what you are doing. And then again...
17
+ API_VERSION = 2.0
18
+ API_HOST = "http://api.defensio.com"
19
+
20
+ # You should't modify anything below this line.
21
+ LIB_VERSION = "0.9"
22
+ ROOT_NODE = "defensio-result"
23
+ FORMAT = :yaml
24
+ USER_AGENT = "Defensio-Ruby #{LIB_VERSION}"
25
+ CLIENT = "Defensio-Ruby | #{LIB_VERSION} | Carl Mercier | cmercier@websense.com"
26
+ KEEP_ALIVE = false
27
+
28
+ attr_reader :http_session, :client
29
+
30
+ def initialize(api_key, client = CLIENT)
31
+ @client = client
32
+ @api_key = api_key
33
+ end
34
+
35
+ # Get information about the api key
36
+ def get_user
37
+ call :get, api_url
38
+ end
39
+
40
+ # Create and analyze a new document
41
+ # @param [Hash] data The parameters to be sent to Defensio. Keys can either be Strings or Symbols
42
+ # @return [Array] An array containing 2 values: the HTTP status code & a Hash with the values returned by Defensio
43
+ def post_document(data)
44
+ data = { :client => @client }.merge(data)
45
+ call :post, api_url("documents"), data
46
+ end
47
+
48
+ # Get the status of an existing document
49
+ # @param [String] signature The signature of the document to retrieve
50
+ # @return [Array] An array containing 2 values: the HTTP status code & a Hash with the values returned by Defensio
51
+ def get_document(signature)
52
+ call :get, api_url("documents", signature)
53
+ end
54
+
55
+ # Modify the properties of an existing document
56
+ # @param [String] signature The signature of the document to modify
57
+ # @param [Hash] data The parameters to be sent to Defensio. Keys can either be Strings or Symbols
58
+ # @return [Array] An array containing 2 values: the HTTP status code & a Hash with the values returned by Defensio
59
+ def put_document(signature, data)
60
+ call :put, api_url("documents", signature), data
61
+ end
62
+
63
+ # Get basic statistics for the current user
64
+ # @return [Array] An array containing 2 values: the HTTP status code & a Hash with the values returned by Defensio
65
+ def get_basic_stats
66
+ call :get, api_url("basic-stats")
67
+ end
68
+
69
+ # Get more exhaustive statistics for the current user
70
+ # @param [Hash] data The parameters to be sent to Defensio. Keys can either be Strings or Symbols
71
+ # @return [Array] An array containing 2 values: the HTTP status code & a Hash with the values returned by Defensio
72
+ def get_extended_stats(data)
73
+ result = call(:get, api_url("extended-stats"), data)
74
+ 0.upto(result[1]["data"].size - 1) do |i|
75
+ result[1]["data"][i]["date"] = Date.parse(result[1]["data"][i]["date"])
76
+ end
77
+
78
+ result
79
+ end
80
+
81
+ # Filter a set of values based on a pre-defined dictionary
82
+ def post_profanity_filter(data)
83
+ call :post, api_url("profanity-filter"), data
84
+ end
85
+
86
+ # Takes the request object (Rails, Sinatra, Merb) of an async request callback and returns a hash
87
+ # containing the status of the document being analyzed.
88
+ # @param [ActionController::Request, Sinatra::Request, String] request The request object created after Defensio POSTed to your site, or a string representation of the POST data.
89
+ # @return [Hash] Status of the document
90
+ def handle_post_document_async_callback(request)
91
+ if request.is_a?(String)
92
+ data = request
93
+ elsif request.respond_to?(:body) && request.body.is_a?(StringIO)
94
+ data = request.body.read
95
+ else
96
+ raise ArgumentError, "Unknown request type: #{request.class}"
97
+ end
98
+
99
+ parse_body(data)
100
+ end
101
+
102
+ # See handle_post_document_async_callback
103
+ def self.handle_post_document_async_callback(request)
104
+ Defensio.new(nil).handle_post_document_async_callback(request)
105
+ end
106
+
107
+ protected
108
+ def api_url(action = nil, id = nil)
109
+ path = "#{API_HOST}/#{API_VERSION}/users/#{@api_key}"
110
+ path += "/#{action}" if action
111
+ path += "/#{id}" if id
112
+ path += ".#{FORMAT}"
113
+ end
114
+
115
+ def http_session
116
+ return @http_session if KEEP_ALIVE && @http_session
117
+ @http_session = Patron::Session.new
118
+ @http_session.timeout = 20
119
+ @http_session.headers['User-Agent'] = USER_AGENT
120
+ @http_session.headers['Content-Type'] = "text/#{FORMAT}"
121
+ @http_session
122
+ end
123
+
124
+ def http_session=(session)
125
+ @http_session = session
126
+ end
127
+
128
+ def call(method, url, data = nil)
129
+ data = hash_to_query_string(data) if data.is_a?(Hash)
130
+ url = url + "?#{data}" unless data.nil? || data.empty?
131
+
132
+ response = case method
133
+ when :get
134
+ http_session.get(url)
135
+ when :delete
136
+ http_session.delete(url)
137
+ when :post
138
+ http_session.post(url, {})
139
+ when :put
140
+ http_session.put(url, {})
141
+ else
142
+ raise(ArgumentError, "Invalid HTTP method: #{method}")
143
+ end
144
+
145
+ http_session = nil unless KEEP_ALIVE
146
+
147
+ [response.status, parse_body(response.body)]
148
+ end
149
+
150
+ def parse_body(str)
151
+ if FORMAT == :yaml
152
+ return YAML::load(str)[ROOT_NODE]
153
+ else
154
+ raise(NotImplementedError, "This library doesn't support this format: #{FORMAT}")
155
+ end
156
+ end
157
+
158
+ def hash_to_query_string(data)
159
+ return nil unless data.is_a?(Hash)
160
+ out = ""
161
+ sort_hash_by_key(data).each do |item|
162
+ k, v = item[0], item[1]
163
+ out += "&" unless out.empty?
164
+ k = k.to_s.gsub(/_/, "-") if k.is_a?(Symbol)
165
+ out += "#{k}=#{URI.escape(v.to_s)}"
166
+ end
167
+ out
168
+ end
169
+
170
+ def sort_hash_by_key(hash)
171
+ hash.keys.sort_by {|s| s.to_s}.map {|key| [key, hash[key]] }
172
+ end
173
+ end
@@ -0,0 +1,288 @@
1
+ require File.dirname(__FILE__) + "/../lib/defensio"
2
+ DEFENSIO_ENV = "test"
3
+ require 'test/unit'
4
+ require 'mocha'
5
+ require 'redgreen'
6
+ require 'ostruct'
7
+
8
+ class DefensioTest < Test::Unit::TestCase
9
+ MOCK_RESPONSE = true
10
+ API_KEY = "1234567890"
11
+ OWNER_URL = "http://example.org"
12
+ SIGNATURE = "abcdefghijklmnop"
13
+
14
+ API_VERSION = 2.0
15
+ API_HOST = "http://api.defensio.com"
16
+ FORMAT = :yaml
17
+ HEADERS = {"User-Agent" => "Defensio-Ruby #{Defensio::LIB_VERSION}", "Content-Type" => "text/yaml"}
18
+
19
+ # API METHOD TESTS -- Useful to learn how to use the library
20
+ def test_get_user
21
+ if MOCK_RESPONSE
22
+ Patron::Session.any_instance.expects(:get).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}.#{FORMAT}").once.returns(FakePatronResponse.new(200, user_body))
23
+ end
24
+
25
+ status, body = @d.get_user
26
+ assert body.is_a?(Hash)
27
+ assert_equal 200, status
28
+ assert_equal "success", body["status"]
29
+ end
30
+
31
+ def test_post_document
32
+ if MOCK_RESPONSE
33
+ query = "client=Defensio-Ruby%20%7C%20#{Defensio::LIB_VERSION}%20%7C%20Carl%20Mercier%20%7C%20cmercier@websense.com&content=We%20sell%20cheap%20Viagra!%20[spam,0.95]&platform=my_awesome_app&type=test"
34
+ Patron::Session.any_instance.expects(:post).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/documents.#{FORMAT}?#{query}", {}).once.returns(FakePatronResponse.new(200, document_body(SIGNATURE)))
35
+ end
36
+
37
+ data = { :content => "We sell cheap Viagra! [spam,0.95]", :platform => "my_awesome_app", :type => "test" }
38
+ status, body = @d.post_document(data)
39
+ assert body.is_a?(Hash)
40
+ assert_equal 200, status
41
+ assert_equal "success", body["status"]
42
+ assert body["signature"].is_a?(String)
43
+ end
44
+
45
+ def test_get_document
46
+ if MOCK_RESPONSE
47
+ Patron::Session.any_instance.expects(:get).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/documents/#{SIGNATURE}.#{FORMAT}").once.returns(FakePatronResponse.new(200, document_body(SIGNATURE)))
48
+ end
49
+
50
+ status, body = @d.get_document(SIGNATURE)
51
+ assert body.is_a?(Hash)
52
+ assert_equal 200, status
53
+ assert_equal "success", body["status"]
54
+ assert_equal SIGNATURE, body["signature"]
55
+ end
56
+
57
+ def test_put_document
58
+ if MOCK_RESPONSE
59
+ query = "allow=true"
60
+ Patron::Session.any_instance.expects(:put).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/documents/#{SIGNATURE}.#{FORMAT}?#{query}", {}).once.returns(FakePatronResponse.new(200, document_body_allowed(SIGNATURE)))
61
+ end
62
+
63
+ status, body = @d.put_document(SIGNATURE, :allow => true)
64
+ assert body.is_a?(Hash)
65
+ assert_equal 200, status
66
+ assert_equal "success", body["status"]
67
+ assert_equal SIGNATURE, body["signature"]
68
+ assert_equal true, body["allow"]
69
+ end
70
+
71
+ def test_get_basic_stats
72
+ if MOCK_RESPONSE
73
+ Patron::Session.any_instance.expects(:get).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/basic-stats.#{FORMAT}").once.returns(FakePatronResponse.new(200, basic_stats_body))
74
+ end
75
+
76
+ status, body = @d.get_basic_stats
77
+ assert body.is_a?(Hash)
78
+ assert_equal 200, status
79
+ assert_equal "success", body["status"]
80
+ assert body["unwanted"]["total"].is_a?(Integer)
81
+ end
82
+
83
+ def test_get_extended_stats
84
+ if MOCK_RESPONSE
85
+ query="from=2009-09-01&to=2009-09-03"
86
+ Patron::Session.any_instance.expects(:get).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/extended-stats.#{FORMAT}?#{query}").once.returns(FakePatronResponse.new(200, extended_stats_body))
87
+ end
88
+
89
+ status, body = @d.get_extended_stats(:from => Date.new(2009, 9, 1), :to => Date.new(2009, 9, 3))
90
+ assert body.is_a?(Hash)
91
+ assert_equal 200, status
92
+ assert_equal "success", body["status"]
93
+ assert body["data"].is_a?(Array)
94
+ assert body["data"][0]["date"].is_a?(Date)
95
+ end
96
+
97
+ def test_post_profanity_filter
98
+ if MOCK_RESPONSE
99
+ query="OtherField=hello%20again&field1=hello%20world"
100
+ Patron::Session.any_instance.expects(:post).with("#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/profanity-filter.#{FORMAT}?#{query}", {}).once.returns(FakePatronResponse.new(200, profanity_filter_body))
101
+ end
102
+
103
+ status, body = @d.post_profanity_filter("field1"=>"hello world", "OtherField"=>"hello again")
104
+ assert body.is_a?(Hash)
105
+ assert_equal 200, status
106
+ assert_equal "success", body["status"]
107
+ assert body["filtered"].is_a?(Hash)
108
+ assert body["filtered"].keys.include?("field1")
109
+ assert body["filtered"].keys.include?("OtherField")
110
+ end
111
+
112
+ def test_handle_post_document_async_callback__string
113
+ result = { "api-version" => API_VERSION,
114
+ "status" => "success",
115
+ "message" => nil,
116
+ "signature" => SIGNATURE,
117
+ "allow" => false,
118
+ "classification" => "malicious",
119
+ "spaminess" => 0.95,
120
+ "profanity-match" => true }
121
+
122
+ assert_equal result, @d.class.handle_post_document_async_callback(document_body(SIGNATURE))
123
+ assert_equal result, @d.handle_post_document_async_callback(document_body(SIGNATURE))
124
+ end
125
+
126
+ def test_handle_post_document_async_callback__request_object
127
+ result = { "api-version" => API_VERSION,
128
+ "status" => "success",
129
+ "message" => nil,
130
+ "signature" => SIGNATURE,
131
+ "allow" => false,
132
+ "classification" => "malicious",
133
+ "spaminess" => 0.95,
134
+ "profanity-match" => true }
135
+
136
+ fake_request_object = OpenStruct.new(:body => StringIO.new(document_body(SIGNATURE)))
137
+ assert_equal result, @d.class.handle_post_document_async_callback(fake_request_object)
138
+
139
+ fake_request_object = OpenStruct.new(:body => StringIO.new(document_body(SIGNATURE)))
140
+ assert_equal result, @d.handle_post_document_async_callback(fake_request_object)
141
+ end
142
+
143
+ def test_handle_post_document_async_callback__invalid_object_type
144
+ assert_raise(ArgumentError) { @d.class.handle_post_document_async_callback(nil) }
145
+ assert_raise(ArgumentError) { @d.handle_post_document_async_callback(nil) }
146
+ end
147
+
148
+
149
+ # OTHER TESTS
150
+ def test_http_session
151
+ s = @d.send(:http_session)
152
+ assert s.is_a?(Patron::Session)
153
+ if @d.class.class_eval("KEEP_ALIVE")
154
+ assert_equal s, @d.send(:http_session) # make sure sessions are reused when KEEP_ALIVE is true
155
+ else
156
+ assert_not_equal s, @d.send(:http_session) # make sure sessions are dropped when KEEP_ALIVE is false
157
+ end
158
+
159
+ assert_equal HEADERS, s.headers
160
+ end
161
+
162
+ def test_api_url
163
+ assert_equal "#{API_HOST}/#{API_VERSION}/users/#{API_KEY}.yaml", @d.send(:api_url)
164
+ assert_equal "#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/documents.yaml", @d.send(:api_url, "documents")
165
+ assert_equal "#{API_HOST}/#{API_VERSION}/users/#{API_KEY}/documents/abcdefghijklmnop.yaml", @d.send(:api_url, "documents", "abcdefghijklmnop")
166
+ end
167
+
168
+ def test_hash_to_query_string
169
+ assert_equal "hello-world=true", @d.send(:hash_to_query_string, {:hello_world => true} )
170
+ assert_equal "hello-world=this%20has%20spaces%20and%20characters%20($%20?%20&%20%5E%20%C3%A9)%20that%20should%20be%20escaped",
171
+ @d.send(:hash_to_query_string, {:hello_world => "this has spaces and characters ($ ? & ^ é) that should be escaped"} )
172
+ assert_equal "value1=true&value2=true&value3=true", @d.send(:hash_to_query_string, { "value1" => true, "value2" => true, :value3 => true})
173
+ assert_equal "date=2009-01-01", @d.send(:hash_to_query_string, { "date" => Date.new(2009,1,1) })
174
+ assert_nil @d.send(:hash_to_query_string, nil)
175
+ end
176
+
177
+ def test_parse_body
178
+ parsed = {"hello"=>"world"}
179
+ assert_equal parsed, @d.send(:parse_body, "---\ndefensio-result:\n hello: world")
180
+ end
181
+
182
+ # HELPERS AND SETUP
183
+ def setup
184
+ @d = Defensio.new(API_KEY)
185
+ end
186
+
187
+ def http_session
188
+ @d.send(:http_session)
189
+ end
190
+
191
+ def base_url
192
+ "#{API_HOST}/#{API_VERSION}"
193
+ end
194
+
195
+ # MOCKING
196
+ class FakePatronResponse < Struct.new(:status, :body); end
197
+
198
+ def user_body
199
+ { "defensio-result" => {"api-version" => API_VERSION, "status" => "success", "message" => nil, "owner-url" => OWNER_URL} }.send("to_#{FORMAT}")
200
+ end
201
+
202
+ def document_body(signature)
203
+ { "defensio-result" => {
204
+ "api-version" => API_VERSION,
205
+ "status" => "success",
206
+ "message" => nil,
207
+ "signature" => signature,
208
+ "allow" => false,
209
+ "classification" => "malicious",
210
+ "spaminess" => 0.95,
211
+ "profanity-match" => true }
212
+ }.send("to_#{FORMAT}")
213
+ end
214
+
215
+ def document_body_allowed(signature)
216
+ { "defensio-result" => {
217
+ "api-version" => API_VERSION,
218
+ "status" => "success",
219
+ "message" => nil,
220
+ "signature" => signature,
221
+ "allow" => true,
222
+ "classification" => "innocent",
223
+ "spaminess" => 0.95,
224
+ "profanity-match" => true }
225
+ }.send("to_#{FORMAT}")
226
+ end
227
+
228
+ def basic_stats_body
229
+ { "defensio-result" => {
230
+ "api-version" => API_VERSION,
231
+ "status" => "success",
232
+ "message" => nil,
233
+ "recent-accuracy" => 0.9975,
234
+ "legitimate" => { "total" => 100 },
235
+ "unwanted" => { "total" => 100, "malicious" => 50, "spam" => 100},
236
+ "false-positives" => 1,
237
+ "false-negatices" => 2,
238
+ "learning" => true,
239
+ "learning-status" => "Details about learning mode" }
240
+ }.send("to_#{FORMAT}")
241
+ end
242
+
243
+ def extended_stats_body
244
+ { "defensio-result" => {
245
+ "api-version" => API_VERSION,
246
+ "status" => "success",
247
+ "message" => nil,
248
+ "data" => [
249
+ { "date" => "2009-09-01",
250
+ "recent-accuracy" => 0.9975,
251
+ "legitimate" => 100,
252
+ "unwanted" => 500,
253
+ "false-positives" => 1,
254
+ "false-negatives" => 0 },
255
+ {
256
+ "date" => "2009-09-02",
257
+ "recent-accuracy" => 0.9985,
258
+ "legitimate" => 50,
259
+ "unwanted" => 475,
260
+ "false-positives" => 0,
261
+ "false-negatives" => 0 },
262
+ { "date" => "2009-09-03",
263
+ "recent-accuracy" => 0.9992,
264
+ "legitimate" => 100,
265
+ "unwanted" => 500,
266
+ "false-positives" => 1,
267
+ "false-negatives" => 0 }
268
+ ],
269
+ "chart-urls" => {
270
+ "recent-accuracy" => "http://domain.com/chart/123456",
271
+ "total-unwanted" => "http://domain.com/chart/abcdef",
272
+ "total-legitimate" => "http://domain.com/chart/xyzabc" }
273
+ }
274
+ }.send("to_#{FORMAT}")
275
+ end
276
+
277
+ def profanity_filter_body
278
+ { "defensio-result" => {
279
+ "api-version" => API_VERSION,
280
+ "status" => "success",
281
+ "message" => nil,
282
+ "filtered" =>
283
+ { "field1" => "hello world",
284
+ "OtherField" => "hello again" }
285
+ }
286
+ }.send("to_#{FORMAT}")
287
+ end
288
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: defensio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Carl Mercier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-07 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: patron
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.4.4
24
+ version:
25
+ description: Official Ruby library for Defensio 2.0
26
+ email: support@defensio.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README
34
+ files:
35
+ - .gitignore
36
+ - LICENSE
37
+ - README
38
+ - Rakefile
39
+ - VERSION
40
+ - lib/defensio.rb
41
+ - test/defensio_test.rb
42
+ has_rdoc: true
43
+ homepage: http://github.com/defensio/defensio-ruby
44
+ licenses: []
45
+
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --charset=UTF-8
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.3.5
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Official Ruby library for Defensio 2.0
70
+ test_files:
71
+ - test/defensio_test.rb