defender 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ doc
21
+ .yardoc
22
+
23
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Henrik Hodne
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ = defender
2
+
3
+ This is a Ruby wrapper of the Defensio[http://defensio.com] spam filtering API. To use this library, you need an API key from Defensio. Go ahead and {get one}[http://defensio.com/signup/].
4
+
5
+ == Testing
6
+
7
+ To run the tests, you need to pass the api key and owner url with the environment keys API_KEY and API_OWNER_URL, like this:
8
+ rake spec API_KEY="key1234" API_OWNER_URL="http://myblog.com"
9
+ The tests will fail with invalid keys and urls.
10
+
11
+ == Note on Patches/Pull Requests
12
+
13
+ * Fork the project.
14
+ * Make your feature addition or bug fix.
15
+ * Add tests for it. This is important so I don't break it in a
16
+ future version unintentionally.
17
+ * Commit, do not mess with rakefile, version, or history.
18
+ (if you want to have your own version, that is fine but
19
+ bump version in a commit by itself I can ignore when I pull)
20
+ * Send me a pull request. Bonus points for topic branches.
21
+
22
+ == Copyright
23
+
24
+ Copyright (c) 2009 Henrik Hodne. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "defender"
8
+ gem.summary = %Q{Ruby API wrapper for Defensio}
9
+ gem.description = %Q{A wrapper of the Defensio spam filtering service.}
10
+ gem.email = "henrik.hodne@binaryhex.com"
11
+ gem.homepage = "http://github.com/dvyjones/defender"
12
+ gem.authors = ["Henrik Hodne"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_development_dependency "yard", ">= 0"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ begin
37
+ require 'reek/adapters/rake_task'
38
+ Reek::RakeTask.new do |t|
39
+ t.verbose = false
40
+ t.source_files = 'lib/**/*.rb'
41
+ end
42
+ rescue LoadError
43
+ task :reek do
44
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
45
+ end
46
+ end
47
+
48
+ begin
49
+ require 'roodi'
50
+ require 'roodi_task'
51
+ RoodiTask.new do |t|
52
+ t.verbose = false
53
+ end
54
+ rescue LoadError
55
+ task :roodi do
56
+ abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
57
+ end
58
+ end
59
+
60
+ task :default => :spec
61
+
62
+ begin
63
+ require 'yard'
64
+ YARD::Rake::YardocTask.new
65
+ rescue LoadError
66
+ task :yardoc do
67
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
68
+ end
69
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/defender.rb ADDED
@@ -0,0 +1,328 @@
1
+ require 'yaml'
2
+ require 'net/http'
3
+
4
+ class Defender
5
+ # The Defensio API version currently supported by Defender
6
+ API_VERSION = "1.2"
7
+
8
+ DEFAULT_OPTIONS = {
9
+ service_type: "blog",
10
+ api_key: "",
11
+ owner_url: ""
12
+ }
13
+
14
+ ##
15
+ # Raised if an invalid or no API key is given
16
+ class APIKeyError < StandardError; end
17
+
18
+ ##
19
+ # The response from the {Defender#audit_comment} method. Should only be
20
+ # initialized by the library.
21
+ class CommentResponse
22
+ ##
23
+ # A message signature that uniquely identifies the comment in the Defensio
24
+ # system. This signature should be stored by the client for retraining
25
+ # purposes.
26
+ attr_reader :signature
27
+
28
+ ##
29
+ # A value indicating the relative likelihood of the comment being spam.
30
+ # This value should be stored by the client for use in building convenient
31
+ # spam sorting user-interfaces.
32
+ #
33
+ # @return [Float] A value between 0 and 1.
34
+ attr_reader :spaminess
35
+
36
+ ##
37
+ # Initialize a CommentResponse. Should only be called by the library.
38
+ #
39
+ # @param [Hash] response The response from the audit-comment call.
40
+ def initialize(response)
41
+ @signature = response["signature"]
42
+ @spam = response["spam"]
43
+ @spaminess = response["spaminess"].to_f
44
+ end
45
+
46
+ ##
47
+ # Returns true if Defensio marked the comment as spam, returns false
48
+ # otherwise.
49
+ #
50
+ # @return [Boolean]
51
+ def spam?
52
+ @spam
53
+ end
54
+
55
+ def to_s
56
+ @signature
57
+ end
58
+ end
59
+
60
+ ##
61
+ # The response from the {Defender#statistics} method. Should only be
62
+ # initialized by the library.
63
+ class Statistics
64
+ ##
65
+ # Describes the percentage of comments correctly identified as spam/ham by
66
+ # Defensio on this blog.
67
+ #
68
+ # @return [Float<0..1>]
69
+ attr_reader :accuracy
70
+
71
+ ##
72
+ # The number of spam comments caught by the filter.
73
+ attr_reader :spam
74
+
75
+ ##
76
+ # The number of ham (legitimate) comments accepted by the filter.
77
+ attr_reader :ham
78
+
79
+ ##
80
+ # The number of times a legitimate message was retrained from the spambox
81
+ # (i.e. "de-spammed" by the user)
82
+ attr_reader :false_positives
83
+
84
+ ##
85
+ # The number of times a spam message was retrained from comments box (i.e.
86
+ # "de-legitimized" by the user)
87
+ attr_reader :false_negatives
88
+
89
+ ##
90
+ # A boolean value indicating whether Defensio is still in its initial
91
+ # learning phase.
92
+ #
93
+ # @return [Boolean]
94
+ attr_reader :learning
95
+
96
+ ##
97
+ # More details on the reason(s) why Defensio is still in its initial
98
+ # learning phase.
99
+ attr_reader :learning_status
100
+
101
+ def initialize(response)
102
+ @accuracy = response["accuracy"]
103
+ @spam = response["spam"]
104
+ @ham = response["ham"]
105
+ @false_positives = response["false-positives"]
106
+ @false_negatives = response["false-negatives"]
107
+ @learning = response["learning"]
108
+ @learning_status = response["learning-status"]
109
+ end
110
+ end
111
+
112
+ attr_accessor :service_type, :api_key, :owner_url
113
+
114
+ ##
115
+ # Raises a StandardError with the error message from Defensio if the
116
+ # response is a "failed" one.
117
+ #
118
+ # @param [Hash] response The return value from {#call_action}.
119
+ def self.raise_if_error(response)
120
+ if response["status"] == "fail"
121
+ raise StandardError, response["message"]
122
+ end
123
+ response
124
+ end
125
+
126
+ ##
127
+ # Converts a hash with symbol keys and underscores to a hash with string
128
+ # keys and hyphens. Calls #strftime or #to_s on the values.
129
+ #
130
+ # @param [Hash] options Input options.
131
+ # @return [Hash]
132
+ def self.options_to_parameters(options)
133
+ opts = {}
134
+ options.each do |key, value|
135
+ if value.respond_to?(:strftime)
136
+ value = value.strftime("%Y/%m/%d")
137
+ end
138
+ opts[key.to_s.gsub("_", "-").downcase] = value.to_s
139
+ end
140
+ opts
141
+ end
142
+
143
+ ##
144
+ # Initialize Defender
145
+ #
146
+ # @param [Hash] opts The options hash.
147
+ # @option opts ["blog","app"] :service_type ("blog") The service type. May be
148
+ # "app" (use of Defender within an application) or "blog" (use of Defender
149
+ # to support a blogging platform).
150
+ # @option opts [String] :api_key Your API key. This option is required, the
151
+ # method calls will fail without it.
152
+ def initialize(opts={})
153
+ opts = DEFAULT_OPTIONS.merge(opts)
154
+ @service_type = opts[:service_type]
155
+ @api_key = opts[:api_key]
156
+ @owner_url = opts[:owner_url]
157
+ end
158
+
159
+ ##
160
+ # Checks if the given key is valid.
161
+ #
162
+ # @return [Boolean]
163
+ # @see http://defensio.com/api/#validate-key
164
+ def valid_key?
165
+ begin
166
+ response = call_action("validate-key")
167
+ if response["status"] == "success"
168
+ return true
169
+ else
170
+ return false
171
+ end
172
+ rescue StandardError
173
+ return false
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Announce an article existence. This should (if feasible) be called when an
179
+ # article or blogpost is created so Defensio can analyse it.
180
+ #
181
+ # @param [#to_s] title The title of the article
182
+ # @param [#to_s] author The name of the author of the article
183
+ # @param [#to_s] author_email The email address of the person posting the
184
+ # article.
185
+ # @param [#to_s] content The content of the article itself.
186
+ # @param [#to_s] permalink The permalink of the article just posted.
187
+ # @raise [StandardError] If the call fails, a StandardError is raised with
188
+ # the error message given from Defensio.
189
+ # @return [Boolean] Returns true if the article was successfully announced,
190
+ # raises StandardError otherwise.
191
+ # @see http://defensio.com/api/#announce-article
192
+ def announce_article(title, author, author_email, content, permalink)
193
+ response = call_action("announce-article",
194
+ "article-title" => title.to_s,
195
+ "article-author" => author.to_s,
196
+ "article-author-email" => author_email.to_s,
197
+ "article-content" => content,
198
+ "permalink" => permalink)
199
+ true
200
+ end
201
+
202
+ ##
203
+ # Check if a comment is spam. This is the central action of Defensio.
204
+ #
205
+ # @param [Hash] opts All options are recommended, but only required if noted.
206
+ # @option opts [#to_s] :user_ip The IP address of whomever is posting the
207
+ # comment. This option is required.
208
+ # @option opts [#to_s, #strftime] :article_date The date the original blog
209
+ # article was posted. If a string is given, it must be in the format
210
+ # "yyyy/mm/dd". This option is required.
211
+ # @option opts [#to_s] :comment_author The name of the author of the comment.
212
+ # This option is required.
213
+ # @option opts ["comment", "trackback", "pingback", "other"] :comment_type
214
+ # The type of the comment being posted to the blog. This option is required
215
+ # @option opts [#to_s] :comment_content The actual content of the comment
216
+ # (strongly recommended to be included where ever possible).
217
+ # @option opts [#to_s] :comment_author_email The email address of the person
218
+ # posting the comment.
219
+ # @option opts [#to_s] :comment_author_url The URL of the person posting the
220
+ # comment.
221
+ # @option opts [#to_s] :permalink The permalink of the blog post to which
222
+ # the comment is being posted.
223
+ # @option opts [#to_s] :referrer The URL of the site that brought commenter
224
+ # to this page.
225
+ # @option opts [Boolean] :user_logged_in Whether or not the user posting
226
+ # the comment is logged-into the blogging platform
227
+ # @option opts [Boolean] :trusted_user Whether or not the user is an
228
+ # administrator, moderator or editor of this blog; the client should pass
229
+ # true only if blogging platform can guarantee that the user has been
230
+ # authenticated and has a role of responsibility on this blog.
231
+ # @option opts [#to_s] :openid The OpenID URL of the currently logged in
232
+ # user. Must be used in conjunction with :user_logged_in => true. OpenID
233
+ # authentication must be taken care of by your application.
234
+ # @option opts [#to_s] :test_force For testing purposes only: Use this
235
+ # parameter to force the outcome of audit_comment. Optionally affix (with
236
+ # a comma) a desired spaminess return value (in the range 0 to 1).
237
+ # Example: "spam,0.5000" or "ham,0.0010".
238
+ # @raise [StandardError] If the call fails, a StandardError is raised with
239
+ # the error message given from Defensio.
240
+ # @return [Defender::CommentResponse]
241
+ # @see http://defensio.com/api/#audit-comment
242
+ def audit_comment(opts={})
243
+ response = call_action("audit-comment", Defender.options_to_parameters(opts))
244
+ return CommentResponse.new(response)
245
+ end
246
+
247
+ ##
248
+ # This action is used to retrain false negatives. False negatives are
249
+ # comments that were originally tagged as "ham" (i.e. legitimate) but were
250
+ # in fact spam.
251
+ #
252
+ # @param [Array<#to_s, CommentResponse>] signatures List of signatures (may
253
+ # contain a single entry) of the comments to be submitted for retraining.
254
+ # Note that a signature for each comment was originally provided by the
255
+ # {#audit_comment} method.
256
+ # @raise [StandardError] If the call fails, a StandardError is raised with
257
+ # the error message given from Defensio.
258
+ # @return [Boolean] Returns true if the comments were successfully marked,
259
+ # raises StandardError otherwise.
260
+ def report_false_negatives(signatures)
261
+ response = call_action("report-false-negatives",
262
+ "signatures" => signatures.map(&:to_s).join(","))
263
+ true
264
+ end
265
+
266
+ ##
267
+ # This action is used to retrain false negatives. False negatives are
268
+ # comments that were originally tagged as spam but were in fact "ham" (i.e.
269
+ # legitimate).
270
+ #
271
+ # @param [Array<#to_s, CommentResponse>] signatures List of signatures (may
272
+ # contain a single entry) of the comments to be submitted for retraining.
273
+ # Note that a signature for each comment was originally provided by the
274
+ # {#audit_comment} method.
275
+ # @raise [StandardError] If the call fails, a StandardError is raised with
276
+ # the error message given from Defensio.
277
+ # @return [Boolean] Returns true if the comments were successfully marked,
278
+ # raises StandardError otherwise.
279
+ def report_false_positives(signatures)
280
+ response = call_action("report-false-positives",
281
+ "signatures" => signatures.map(&:to_s).join(","))
282
+ true
283
+ end
284
+
285
+ ##
286
+ # This action returns basic statistics regarding the performance of Defensio
287
+ # since activation.
288
+ #
289
+ # @return [Defender::Statistics]
290
+ def statistics
291
+ response = call_action("get-stats")
292
+ return Statistics.new(response)
293
+ end
294
+
295
+ private
296
+ ##
297
+ # Returns the url for the given action.
298
+ #
299
+ # @param [#to_s] action The action to generate the URL for.
300
+ # @return [String] The URL for the action.
301
+ # @raise [APIKeyError] Raises this if no API key is given.
302
+ def url(action)
303
+ raise APIKeyError unless @api_key.length > 0
304
+ "http://api.defensio.com/" \
305
+ "#{@service_type}/" \
306
+ "#{Defender::API_VERSION}/" \
307
+ "#{action}/" \
308
+ "#{@api_key}.yaml"
309
+ end
310
+
311
+ ##
312
+ # Backend function for calling an action.
313
+ #
314
+ # @param [#to_s] action The action to call.
315
+ # @param [Hash] params The parameters for the action.
316
+ # @return [Hash] The raw response, only parsed from YAML.
317
+ # @raise [APIKeyError] If an invalid (or no) API key is given, this is
318
+ # raised
319
+ def call_action(action, params={})
320
+ params = {"owner-url" => @owner_url}.merge(params)
321
+ response = Net::HTTP.post_form(URI.parse(url(action)), params)
322
+ if response.code == 400
323
+ raise APIKeyError
324
+ else
325
+ Defender.raise_if_error(YAML.load(response.body)["defensio-result"])
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,79 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Defender" do
4
+ it "should raise a StandardError if a method fails" do
5
+ lambda do
6
+ Defender.raise_if_error({
7
+ "status" => "fail",
8
+ "message" => "Failed!"
9
+ })
10
+ end.should raise_error(StandardError, "Failed!")
11
+ end
12
+
13
+ it "should return the correct URL for any given action" do
14
+ d = Defender.new(:api_key => "key1234")
15
+ d.instance_eval do
16
+ url("foobar")
17
+ end.should == "http://api.defensio.com/blog/#{Defender::API_VERSION}/foobar/key1234.yaml"
18
+ end
19
+
20
+ it "should correctly identify a valid API key" do
21
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
22
+ d.valid_key?.should be_true
23
+ end
24
+
25
+ it "should correctly identify an invalid API key" do
26
+ d = Defender.new(:api_key => "key1234", :owner_url => ENV["API_OWNER_URL"])
27
+ d.valid_key?.should be_false
28
+ end
29
+
30
+ it "should correctly identify a spammy comment" do
31
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
32
+
33
+ d.audit_comment(
34
+ :user_ip => "127.0.0.1",
35
+ :article_date => Time.now,
36
+ :comment_author => "Henrik Hodne",
37
+ :comment_type => "comment",
38
+ :test_force => "spam,0.5000"
39
+ ).spam?.should be_true
40
+ end
41
+
42
+ it "should correctly identify a meaty comment" do
43
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
44
+
45
+ d.audit_comment(
46
+ :user_ip => "127.0.0.1",
47
+ :article_date => Time.now,
48
+ :comment_author => "Henrik Hodne",
49
+ :comment_type => "comment",
50
+ :test_force => "ham,0.1000"
51
+ ).spam?.should be_false
52
+ end
53
+
54
+ it "should correctly set the spaminess" do
55
+ d = Defender.new(:api_key => ENV["API_KEY"], :owner_url => ENV["API_OWNER_URL"])
56
+
57
+ d.audit_comment(
58
+ :user_ip => "127.0.0.1",
59
+ :article_date => Time.now,
60
+ :comment_author => "Henrik Hodne",
61
+ :comment_type => "comment",
62
+ :test_force => "spam,0.5000"
63
+ ).spaminess.should == 0.5
64
+ end
65
+
66
+ it "should fail without valid API credentials" do
67
+ d = Defender.new(:api_key => "key1234", :owner_url => "http://google.com")
68
+
69
+ lambda {
70
+ d.audit_comment(
71
+ :user_ip => "127.0.0.1",
72
+ :article_date => Time.now,
73
+ :comment_author => "Henrik Hodne",
74
+ :comment_type => "comment",
75
+ :test_force => "ham,0.1000"
76
+ )
77
+ }.should raise_error(StandardError, "Authentication failed. Please verify your key/owner-url combination.")
78
+ end
79
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'defender'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: defender
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henrik Hodne
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-08 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.9
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: yard
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A wrapper of the Defensio spam filtering service.
36
+ email: henrik.hodne@binaryhex.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/defender.rb
52
+ - spec/defender_spec.rb
53
+ - spec/spec.opts
54
+ - spec/spec_helper.rb
55
+ has_rdoc: true
56
+ homepage: http://github.com/dvyjones/defender
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.3.5
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Ruby API wrapper for Defensio
83
+ test_files:
84
+ - spec/spec_helper.rb
85
+ - spec/defender_spec.rb