ratonvirus 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -13
- data/app/validators/antivirus_validator.rb +7 -7
- data/lib/ratonvirus/engine.rb +2 -0
- data/lib/ratonvirus/error.rb +2 -0
- data/lib/ratonvirus/processable.rb +3 -0
- data/lib/ratonvirus/scanner/addon/remove_infected.rb +7 -4
- data/lib/ratonvirus/scanner/base.rb +27 -27
- data/lib/ratonvirus/scanner/eicar.rb +11 -12
- data/lib/ratonvirus/scanner/support/callbacks.rb +26 -27
- data/lib/ratonvirus/storage/active_storage.rb +45 -44
- data/lib/ratonvirus/storage/base.rb +16 -23
- data/lib/ratonvirus/storage/carrierwave.rb +1 -1
- data/lib/ratonvirus/storage/filepath.rb +3 -3
- data/lib/ratonvirus/storage/multi.rb +22 -19
- data/lib/ratonvirus/support/backend.rb +129 -129
- data/lib/ratonvirus/version.rb +1 -1
- data/lib/ratonvirus.rb +11 -11
- data/lib/tasks/ratonvirus.rake +4 -2
- metadata +31 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b47a31e5d403f39f0b7496a292532b2dbaa5c38fe624368b09a2cb28c949623
|
4
|
+
data.tar.gz: 7a37d2f6d7b20e45c417b87ea22bbadbaf434714c1ce7082c1b4a4d87b646845
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a52088960f46b6e4aad10616e9f2b65ad082490bd7158484178f88cc6aa68fa76397e471fa4e791e38cd51bcca799aabdbe868656c03226a9157bb2d82be4aaa
|
7
|
+
data.tar.gz: 62b103a30f7711cad63d9c9095eec42f98baf372b7fe2308ee03dc3ae063a74bf458afa8026e36ac10aa184df40302a9d721eafbbd93f6fc92efcc090efd9c4c
|
data/README.md
CHANGED
@@ -26,13 +26,13 @@ end
|
|
26
26
|
Running manual scans:
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
puts "File contains a virus" if Ratonvirus.scanner.virus?(
|
29
|
+
puts "File contains a virus" if Ratonvirus.scanner.virus?("/path/to/file.pdf")
|
30
30
|
```
|
31
31
|
|
32
32
|
Manual scanning works e.g. for file uploads, file object and file paths.
|
33
33
|
|
34
|
-
Ratonvirus works
|
35
|
-
|
34
|
+
Ratonvirus works with Active Storage out of the box. Support for CarrierWave is
|
35
|
+
also built in, assuming you already have CarrierWave as a dependency.
|
36
36
|
|
37
37
|
## When to use this?
|
38
38
|
|
@@ -53,7 +53,9 @@ as you are used to with many other gems.
|
|
53
53
|
|
54
54
|
You will need to setup a virus scanner on your machine. If you have done that
|
55
55
|
before, configuration should be rather simple. Instructions are provided for
|
56
|
-
the open source ClamAV scanner
|
56
|
+
the open source ClamAV scanner in the
|
57
|
+
[`ratonvirus-clamby`](https://github.com/mainio/ratonvirus-clamby)
|
58
|
+
documentation.
|
57
59
|
|
58
60
|
This gem ships with an example
|
59
61
|
[EICAR file](https://en.wikipedia.org/wiki/EICAR_test_file) scanner to test out
|
@@ -66,7 +68,7 @@ environments.
|
|
66
68
|
Add this line to your application's Gemfile:
|
67
69
|
|
68
70
|
```ruby
|
69
|
-
gem
|
71
|
+
gem "ratonvirus"
|
70
72
|
```
|
71
73
|
|
72
74
|
And then execute:
|
@@ -102,7 +104,7 @@ Ratonvirus correctly configured.
|
|
102
104
|
|
103
105
|
By default Ratonvirus is set to remove all infected files that it detects after
|
104
106
|
scanning them. If you want to remove this functionality, please refer to the
|
105
|
-
[Scanner addons](
|
107
|
+
[Scanner addons](docs/index.md#scanner-addons) section of the developer
|
106
108
|
documentation.
|
107
109
|
|
108
110
|
## Usage
|
@@ -153,7 +155,7 @@ end
|
|
153
155
|
|
154
156
|
# When scanning files, file paths or active storage resources
|
155
157
|
Ratonvirus.configure do |config|
|
156
|
-
config.storage = :multi, [:filepath, :active_storage]
|
158
|
+
config.storage = :multi, {storages: [:filepath, :active_storage]}
|
157
159
|
end
|
158
160
|
```
|
159
161
|
|
@@ -164,7 +166,7 @@ examples:
|
|
164
166
|
# It is a good idea to check first that the scanner is available.
|
165
167
|
if Ratonvirus.scanner.available?
|
166
168
|
# Scanning a file path
|
167
|
-
path =
|
169
|
+
path = "/path/to/file.pdf"
|
168
170
|
if Ratonvirus.scanner.virus?(path)
|
169
171
|
puts "There is a virus at #{path}"
|
170
172
|
end
|
@@ -186,8 +188,8 @@ Here are few sample configurations to speed up the configuration process.
|
|
186
188
|
Gemfile:
|
187
189
|
|
188
190
|
```ruby
|
189
|
-
gem
|
190
|
-
gem
|
191
|
+
gem "ratonvirus"
|
192
|
+
gem "ratonvirus-clamby"
|
191
193
|
```
|
192
194
|
|
193
195
|
Initializer:
|
@@ -209,13 +211,16 @@ class YourModel < ApplicationRecord
|
|
209
211
|
end
|
210
212
|
```
|
211
213
|
|
214
|
+
For installing ClamAV, refer to
|
215
|
+
[`ratonvirus-clamby`](https://github.com/mainio/ratonvirus-clamby)
|
216
|
+
|
212
217
|
#### CarrierWave and ClamAV
|
213
218
|
|
214
219
|
Gemfile:
|
215
220
|
|
216
221
|
```ruby
|
217
|
-
gem
|
218
|
-
gem
|
222
|
+
gem "ratonvirus"
|
223
|
+
gem "ratonvirus-clamby"
|
219
224
|
```
|
220
225
|
|
221
226
|
Initializer:
|
@@ -237,10 +242,13 @@ class YourModel < ApplicationRecord
|
|
237
242
|
end
|
238
243
|
```
|
239
244
|
|
245
|
+
For installing ClamAV, refer to
|
246
|
+
[`ratonvirus-clamby`](https://github.com/mainio/ratonvirus-clamby)
|
247
|
+
|
240
248
|
## Further configuration and development
|
241
249
|
|
242
250
|
For further information about the configurations and how to create custom
|
243
|
-
scanners, please refer to the [documentation](
|
251
|
+
scanners, please refer to the [documentation](docs/index.md).
|
244
252
|
|
245
253
|
## License
|
246
254
|
|
@@ -13,14 +13,14 @@ class AntivirusValidator < ActiveModel::EachValidator
|
|
13
13
|
scanner = Ratonvirus.scanner
|
14
14
|
return unless scanner.available?
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
else
|
22
|
-
record.errors.add attribute, :antivirus_virus_detected
|
16
|
+
return unless scanner.virus?(value)
|
17
|
+
|
18
|
+
if scanner.errors.any?
|
19
|
+
scanner.errors.each do |err|
|
20
|
+
record.errors.add attribute, err
|
23
21
|
end
|
22
|
+
else
|
23
|
+
record.errors.add attribute, :antivirus_virus_detected
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
data/lib/ratonvirus/engine.rb
CHANGED
data/lib/ratonvirus/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Scanner
|
3
5
|
module Addon
|
@@ -7,11 +9,12 @@ module Ratonvirus
|
|
7
9
|
end
|
8
10
|
|
9
11
|
private
|
10
|
-
def remove_infected_file(processable)
|
11
|
-
return unless errors.include?(:antivirus_virus_detected)
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
def remove_infected_file(processable)
|
14
|
+
return unless errors.include?(:antivirus_virus_detected)
|
15
|
+
|
16
|
+
processable.remove
|
17
|
+
end
|
15
18
|
end
|
16
19
|
end
|
17
20
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Scanner
|
3
5
|
class Base
|
@@ -12,7 +14,7 @@ module Ratonvirus
|
|
12
14
|
attr_reader :config
|
13
15
|
attr_reader :errors # Only available after `virus?` has been called.
|
14
16
|
|
15
|
-
def initialize(configuration={})
|
17
|
+
def initialize(configuration = {})
|
16
18
|
@config = default_config.merge!(configuration)
|
17
19
|
|
18
20
|
# Make the following callbacks available:
|
@@ -113,42 +115,40 @@ module Ratonvirus
|
|
113
115
|
end
|
114
116
|
|
115
117
|
protected
|
116
|
-
def default_config
|
117
|
-
{
|
118
|
-
force_availability: false,
|
119
|
-
}
|
120
|
-
end
|
121
118
|
|
122
|
-
|
123
|
-
|
124
|
-
|
119
|
+
def default_config
|
120
|
+
{ force_availability: false }
|
121
|
+
end
|
125
122
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
123
|
+
def storage
|
124
|
+
Ratonvirus.storage
|
125
|
+
end
|
126
|
+
|
127
|
+
def scan(processable)
|
128
|
+
processable.path do |path|
|
129
|
+
run_callbacks :scan, processable do
|
130
|
+
run_scan(path)
|
131
131
|
end
|
132
132
|
end
|
133
|
+
end
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
)
|
138
|
-
end
|
135
|
+
def run_scan(_path)
|
136
|
+
raise NotImplementedError, "Implement run_scan on #{self.class.name}"
|
137
|
+
end
|
139
138
|
|
140
139
|
private
|
141
|
-
# Prepare is called each time before scanning is run. During the first
|
142
|
-
# call to this method, the addons are applied to the scanner instance.
|
143
|
-
def prepare
|
144
|
-
return if @ready
|
145
140
|
|
146
|
-
|
147
|
-
|
148
|
-
|
141
|
+
# Prepare is called each time before scanning is run. During the first
|
142
|
+
# call to this method, the addons are applied to the scanner instance.
|
143
|
+
def prepare
|
144
|
+
return if @ready
|
149
145
|
|
150
|
-
|
146
|
+
Ratonvirus.addons.each do |addon_cls|
|
147
|
+
extend addon_cls
|
151
148
|
end
|
149
|
+
|
150
|
+
@ready = true
|
151
|
+
end
|
152
152
|
end
|
153
153
|
end
|
154
154
|
end
|
@@ -8,7 +8,7 @@ module Ratonvirus
|
|
8
8
|
class Eicar < Base
|
9
9
|
# SHA256 digest of the EICAR test file for virus testing
|
10
10
|
# See: https://en.wikipedia.org/wiki/EICAR_test_file
|
11
|
-
EICAR_SHA256 =
|
11
|
+
EICAR_SHA256 = "131f95c51cc819465fa1797f6ccacf9d494aaaff46fa3eac73ae63ffbdfd8267"
|
12
12
|
|
13
13
|
class << self
|
14
14
|
def executable?
|
@@ -17,18 +17,17 @@ module Ratonvirus
|
|
17
17
|
end
|
18
18
|
|
19
19
|
protected
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
end
|
29
|
-
rescue
|
30
|
-
errors << :antivirus_client_error
|
20
|
+
|
21
|
+
def run_scan(path)
|
22
|
+
if !File.file?(path)
|
23
|
+
errors << :antivirus_file_not_found
|
24
|
+
else
|
25
|
+
sha256 = Digest::SHA256.file path
|
26
|
+
errors << :antivirus_virus_detected if sha256 == EICAR_SHA256
|
31
27
|
end
|
28
|
+
rescue StandardError
|
29
|
+
errors << :antivirus_client_error
|
30
|
+
end
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Scanner
|
3
5
|
module Support
|
@@ -44,40 +46,37 @@ module Ratonvirus
|
|
44
46
|
# end
|
45
47
|
module Callbacks
|
46
48
|
private
|
47
|
-
def run_callbacks(type, *args, &block)
|
48
|
-
if @_callbacks.nil?
|
49
|
-
raise NotDefinedError.new("No callbacks defined")
|
50
|
-
end
|
51
|
-
if @_callbacks[type].nil?
|
52
|
-
raise NotDefinedError.new("Callbacks for #{type} not defined")
|
53
|
-
end
|
54
49
|
|
55
|
-
|
56
|
-
|
57
|
-
|
50
|
+
def run_callbacks(type, *args, &_block)
|
51
|
+
raise NotDefinedError, "No callbacks defined" if @_callbacks.nil?
|
52
|
+
raise NotDefinedError, "Callbacks for #{type} not defined" if @_callbacks[type].nil?
|
58
53
|
|
59
|
-
|
60
|
-
|
54
|
+
run_callback_callables @_callbacks[type][:before], *args
|
55
|
+
result = yield(*args)
|
56
|
+
run_callback_callables @_callbacks[type][:after], *args
|
61
57
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_callback_callables(callables, *args)
|
62
|
+
callables.each do |callable|
|
63
|
+
send(callable, *args)
|
66
64
|
end
|
65
|
+
end
|
67
66
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
67
|
+
def define_callbacks(type)
|
68
|
+
@_callbacks ||= {}
|
69
|
+
@_callbacks[type] ||= {}
|
70
|
+
@_callbacks[type][:before] = []
|
71
|
+
@_callbacks[type][:after] = []
|
73
72
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
73
|
+
define_singleton_method "before_#{type}" do |callable|
|
74
|
+
@_callbacks[type][:before] << callable
|
75
|
+
end
|
76
|
+
define_singleton_method "after_#{type}" do |callable|
|
77
|
+
@_callbacks[type][:after] << callable
|
80
78
|
end
|
79
|
+
end
|
81
80
|
end
|
82
81
|
end
|
83
82
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Ratonvirus
|
4
4
|
module Storage
|
5
5
|
class ActiveStorage < Base
|
6
|
-
def changed?(
|
6
|
+
def changed?(_record, _attribute)
|
7
7
|
# With Active Storage we assume the record is always changed because
|
8
8
|
# there is currently no way to know if the attribute has actually
|
9
9
|
# changed.
|
@@ -22,22 +22,20 @@ module Ratonvirus
|
|
22
22
|
|
23
23
|
def accept?(resource)
|
24
24
|
resource.is_a?(::ActiveStorage::Attached::One) ||
|
25
|
-
|
25
|
+
resource.is_a?(::ActiveStorage::Attached::Many)
|
26
26
|
end
|
27
27
|
|
28
|
-
def process(resource, &
|
28
|
+
def process(resource, &_block)
|
29
29
|
return unless block_given?
|
30
30
|
return if resource.nil?
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
yield processable(attachment)
|
40
|
-
end
|
32
|
+
return unless resource.attached?
|
33
|
+
|
34
|
+
if resource.is_a?(::ActiveStorage::Attached::One)
|
35
|
+
yield processable(resource.attachment) if resource.attachment
|
36
|
+
elsif resource.is_a?(::ActiveStorage::Attached::Many)
|
37
|
+
resource.attachments.each do |attachment|
|
38
|
+
yield processable(attachment)
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
@@ -55,42 +53,45 @@ module Ratonvirus
|
|
55
53
|
end
|
56
54
|
|
57
55
|
private
|
58
|
-
# This creates a local copy of the blob for the scanning process. A
|
59
|
-
# local copy is needed for processing because the actual blob may be
|
60
|
-
# stored at a remote storage service (such as Amazon S3), meaning it
|
61
|
-
# cannot be otherwise processed locally.
|
62
|
-
#
|
63
|
-
# NOTE:
|
64
|
-
# Later implementations of Active Storage have the blob.open method that
|
65
|
-
# provides similar functionality. However, Rails 5.2.x still does not
|
66
|
-
# include this functionality, so we need to take care of it ourselves.
|
67
|
-
#
|
68
|
-
# This was introduced in the following commit:
|
69
|
-
# https://github.com/rails/rails/commit/ee21b7c2eb64def8f00887a9fafbd77b85f464f1
|
70
|
-
#
|
71
|
-
# SEE:
|
72
|
-
# https://edgeguides.rubyonrails.org/active_storage_overview.html#downloading-files
|
73
|
-
def blob_path(blob)
|
74
|
-
tempfile = Tempfile.open(
|
75
|
-
["Ratonvirus", blob.filename.extension_with_delimiter ],
|
76
|
-
tempdir
|
77
|
-
)
|
78
56
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
57
|
+
# This creates a local copy of the blob for the scanning process. A
|
58
|
+
# local copy is needed for processing because the actual blob may be
|
59
|
+
# stored at a remote storage service (such as Amazon S3), meaning it
|
60
|
+
# cannot be otherwise processed locally.
|
61
|
+
#
|
62
|
+
# NOTE:
|
63
|
+
# Later implementations of Active Storage have the blob.open method that
|
64
|
+
# provides similar functionality. However, Rails 5.2.x still does not
|
65
|
+
# include this functionality, so we need to take care of it ourselves.
|
66
|
+
#
|
67
|
+
# This was introduced in the following commit:
|
68
|
+
# https://github.com/rails/rails/commit/ee21b7c2eb64def8f00887a9fafbd77b85f464f1
|
69
|
+
#
|
70
|
+
# SEE:
|
71
|
+
# https://edgeguides.rubyonrails.org/active_storage_overview.html#downloading-files
|
72
|
+
def blob_path(blob)
|
73
|
+
tempfile = Tempfile.open(
|
74
|
+
["Ratonvirus", blob.filename.extension_with_delimiter],
|
75
|
+
tempdir
|
76
|
+
)
|
84
77
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
78
|
+
begin
|
79
|
+
tempfile.binmode
|
80
|
+
blob.download { |chunk| tempfile.write(chunk) }
|
81
|
+
tempfile.flush
|
82
|
+
tempfile.rewind
|
90
83
|
|
91
|
-
|
92
|
-
|
84
|
+
yield tempfile.path
|
85
|
+
rescue StandardError
|
86
|
+
return
|
87
|
+
ensure
|
88
|
+
tempfile.close!
|
93
89
|
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def tempdir
|
93
|
+
Dir.tmpdir
|
94
|
+
end
|
94
95
|
end
|
95
96
|
end
|
96
97
|
end
|
@@ -1,14 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Storage
|
3
5
|
class Base
|
4
6
|
attr_reader :config
|
5
7
|
|
6
|
-
def initialize(configuration={})
|
8
|
+
def initialize(configuration = {})
|
7
9
|
@config = configuration.dup
|
8
10
|
|
9
|
-
if respond_to?(:setup)
|
10
|
-
setup
|
11
|
-
end
|
11
|
+
setup if respond_to?(:setup)
|
12
12
|
end
|
13
13
|
|
14
14
|
# Default process implementation.
|
@@ -23,34 +23,27 @@ module Ratonvirus
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def changed?(
|
27
|
-
raise NotImplementedError.
|
28
|
-
"Implement changed? on #{self.class.name}"
|
29
|
-
)
|
26
|
+
def changed?(_record, _attribute)
|
27
|
+
raise NotImplementedError, "Implement changed? on #{self.class.name}"
|
30
28
|
end
|
31
29
|
|
32
|
-
def accept?(
|
33
|
-
raise NotImplementedError.
|
34
|
-
"Implement accept? on #{self.class.name}"
|
35
|
-
)
|
30
|
+
def accept?(_resource)
|
31
|
+
raise NotImplementedError, "Implement accept? on #{self.class.name}"
|
36
32
|
end
|
37
33
|
|
38
|
-
def asset_path(
|
39
|
-
raise NotImplementedError.
|
40
|
-
"Implement path on #{self.class.name}"
|
41
|
-
)
|
34
|
+
def asset_path(_asset)
|
35
|
+
raise NotImplementedError, "Implement path on #{self.class.name}"
|
42
36
|
end
|
43
37
|
|
44
|
-
def asset_remove(
|
45
|
-
raise NotImplementedError.
|
46
|
-
"Implement remove on #{self.class.name}"
|
47
|
-
)
|
38
|
+
def asset_remove(_asset)
|
39
|
+
raise NotImplementedError, "Implement remove on #{self.class.name}"
|
48
40
|
end
|
49
41
|
|
50
42
|
protected
|
51
|
-
|
52
|
-
|
53
|
-
|
43
|
+
|
44
|
+
def processable(asset)
|
45
|
+
Processable.new(self, asset)
|
46
|
+
end
|
54
47
|
end
|
55
48
|
end
|
56
49
|
end
|
@@ -1,10 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Storage
|
3
5
|
class Filepath < Base
|
4
6
|
def changed?(record, attribute)
|
5
|
-
if record.respond_to? :"#{attribute}_changed?"
|
6
|
-
return record.public_send :"#{attribute}_changed?"
|
7
|
-
end
|
7
|
+
return record.public_send :"#{attribute}_changed?" if record.respond_to? :"#{attribute}_changed?"
|
8
8
|
|
9
9
|
# Some backends do not implement the `attribute_changed?` methods for
|
10
10
|
# the file resources. In that case our best guess is to check whether
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Storage
|
3
5
|
# Multi storage allows the developers to configure multiple storage backends
|
@@ -12,18 +14,18 @@ module Ratonvirus
|
|
12
14
|
def setup
|
13
15
|
@storages = []
|
14
16
|
|
15
|
-
|
16
|
-
config[:storages].each do |storage|
|
17
|
-
if storage.is_a?(Array)
|
18
|
-
type = storage[0]
|
19
|
-
storage_config = storage[1]
|
20
|
-
else
|
21
|
-
type = storage
|
22
|
-
end
|
17
|
+
return unless config[:storages].is_a?(Array)
|
23
18
|
|
24
|
-
|
25
|
-
|
19
|
+
config[:storages].each do |storage|
|
20
|
+
if storage.is_a?(Array)
|
21
|
+
type = storage[0]
|
22
|
+
storage_config = storage[1]
|
23
|
+
else
|
24
|
+
type = storage
|
26
25
|
end
|
26
|
+
|
27
|
+
cls = Ratonvirus.backend_class("Storage", type)
|
28
|
+
@storages << cls.new(storage_config || {})
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
@@ -55,7 +57,7 @@ module Ratonvirus
|
|
55
57
|
|
56
58
|
# Check if any of the storages accept the resource.
|
57
59
|
def accept?(resource)
|
58
|
-
storage_for(resource) do |
|
60
|
+
storage_for(resource) do |_storage|
|
59
61
|
return true
|
60
62
|
end
|
61
63
|
|
@@ -63,16 +65,17 @@ module Ratonvirus
|
|
63
65
|
end
|
64
66
|
|
65
67
|
private
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
68
|
+
|
69
|
+
# Iterates through the @storages array and yields the first storage
|
70
|
+
# that accepts the resource.
|
71
|
+
def storage_for(resource)
|
72
|
+
@storages.each do |storage|
|
73
|
+
if storage.accept?(resource)
|
74
|
+
yield storage
|
75
|
+
break
|
74
76
|
end
|
75
77
|
end
|
78
|
+
end
|
76
79
|
end
|
77
80
|
end
|
78
81
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ratonvirus
|
2
4
|
module Support
|
3
5
|
# The backend implementation allows us to set different backends on the main
|
@@ -30,152 +32,150 @@ module Ratonvirus
|
|
30
32
|
|
31
33
|
subclass = ActiveSupport::Inflector.camelize(backend_type.to_s)
|
32
34
|
ActiveSupport::Inflector.constantize(
|
33
|
-
"#{
|
35
|
+
"#{name}::#{backend_cls}::#{subclass}"
|
34
36
|
)
|
35
37
|
end
|
36
38
|
|
37
39
|
private
|
38
|
-
# Defines the "backend" methods.
|
39
|
-
#
|
40
|
-
# For example, this:
|
41
|
-
# define_backend :foo, 'Foo'
|
42
|
-
#
|
43
|
-
# Would define the following methods:
|
44
|
-
# # Getter for foo
|
45
|
-
# def self.foo
|
46
|
-
# @foo ||= create_foo
|
47
|
-
# end
|
48
|
-
#
|
49
|
-
# # Setter for foo
|
50
|
-
# def self.foo=(foo_type)
|
51
|
-
# set_backend(
|
52
|
-
# :foo,
|
53
|
-
# 'Foo',
|
54
|
-
# foo_type
|
55
|
-
# )
|
56
|
-
# end
|
57
|
-
#
|
58
|
-
# # Destroys the currently active foo.
|
59
|
-
# # The foo is re-initialized when the getter is called.
|
60
|
-
# def self.destroy_foo
|
61
|
-
# @foo = nil
|
62
|
-
# end
|
63
|
-
#
|
64
|
-
# private
|
65
|
-
# def self.create_foo
|
66
|
-
# if @foo_defs.nil?
|
67
|
-
# raise NotDefinedError.new("Foo not defined!")
|
68
|
-
# end
|
69
|
-
#
|
70
|
-
# @foo_defs[:klass].new(@foo_defs[:config])
|
71
|
-
# end
|
72
|
-
#
|
73
|
-
# Usage (getter):
|
74
|
-
# Ratonvirus.foo
|
75
|
-
#
|
76
|
-
# Usage (setter):
|
77
|
-
# Ratonvirus.foo = :bar
|
78
|
-
# Ratonvirus.foo = :bar, {option: 'value'}
|
79
|
-
# Ratonvirus.foo = Ratonvirus::Foo::Bar.new
|
80
|
-
# Ratonvirus.foo = Ratonvirus::Foo::Bar.new({option: 'value'})
|
81
|
-
#
|
82
|
-
# Usage (destroyer):
|
83
|
-
# Ratonvirus.destroy_foo
|
84
|
-
#
|
85
|
-
def define_backend(backend_type, backend_subclass)
|
86
|
-
self.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
87
|
-
# Getter for #{backend_type}
|
88
|
-
def self.#{backend_type}
|
89
|
-
@#{backend_type} ||= create_#{backend_type}
|
90
|
-
end
|
91
40
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
41
|
+
# Defines the "backend" methods.
|
42
|
+
#
|
43
|
+
# For example, this:
|
44
|
+
# define_backend :foo, 'Foo'
|
45
|
+
#
|
46
|
+
# Would define the following methods:
|
47
|
+
# # Getter for foo
|
48
|
+
# def self.foo
|
49
|
+
# @foo ||= create_foo
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # Setter for foo
|
53
|
+
# def self.foo=(foo_type)
|
54
|
+
# set_backend(
|
55
|
+
# :foo,
|
56
|
+
# 'Foo',
|
57
|
+
# foo_type
|
58
|
+
# )
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# # Destroys the currently active foo.
|
62
|
+
# # The foo is re-initialized when the getter is called.
|
63
|
+
# def self.destroy_foo
|
64
|
+
# @foo = nil
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# private
|
68
|
+
# def self.create_foo
|
69
|
+
# if @foo_defs.nil?
|
70
|
+
# raise NotDefinedError.new("Foo not defined!")
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @foo_defs[:klass].new(@foo_defs[:config])
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Usage (getter):
|
77
|
+
# Ratonvirus.foo
|
78
|
+
#
|
79
|
+
# Usage (setter):
|
80
|
+
# Ratonvirus.foo = :bar
|
81
|
+
# Ratonvirus.foo = :bar, {option: 'value'}
|
82
|
+
# Ratonvirus.foo = Ratonvirus::Foo::Bar.new
|
83
|
+
# Ratonvirus.foo = Ratonvirus::Foo::Bar.new({option: 'value'})
|
84
|
+
#
|
85
|
+
# Usage (destroyer):
|
86
|
+
# Ratonvirus.destroy_foo
|
87
|
+
#
|
88
|
+
def define_backend(backend_type, backend_subclass)
|
89
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
90
|
+
# Getter for #{backend_type}
|
91
|
+
def self.#{backend_type}
|
92
|
+
@#{backend_type} ||= create_#{backend_type}
|
93
|
+
end
|
100
94
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
95
|
+
# Setter for #{backend_type}
|
96
|
+
def self.#{backend_type}=(#{backend_type}_value)
|
97
|
+
set_backend(
|
98
|
+
:#{backend_type},
|
99
|
+
"#{backend_subclass}",
|
100
|
+
#{backend_type}_value
|
101
|
+
)
|
102
|
+
end
|
106
103
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
end
|
104
|
+
# Destroys the currently active #{backend_type}.
|
105
|
+
# The #{backend_type} is re-initialized when the getter is called.
|
106
|
+
def self.destroy_#{backend_type}
|
107
|
+
@#{backend_type} = nil
|
108
|
+
end
|
113
109
|
|
114
|
-
|
115
|
-
|
116
|
-
|
110
|
+
# Creates a new backend instance
|
111
|
+
# private
|
112
|
+
def self.create_#{backend_type}
|
113
|
+
if @#{backend_type}_defs.nil?
|
114
|
+
raise NotDefinedError.new("#{backend_subclass} not defined!")
|
117
115
|
end
|
118
|
-
private_class_method :create_#{backend_type}
|
119
|
-
CODE
|
120
|
-
end
|
121
116
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
# The first argument, "backend_type" is the type of backend we are
|
130
|
-
# configuring, e.g. `:scanner`.
|
131
|
-
#
|
132
|
-
# The second argument "backend_cls" is the backend subclass that is
|
133
|
-
# used in the module's namespace, e.g. "Scanner". This would refer to
|
134
|
-
# subclasses `Ratonvirus::Scanner::...`.
|
135
|
-
#
|
136
|
-
# The third argument "backend_value" is the actual value the user
|
137
|
-
# provided for the setter method, e.g. `:eicar` or
|
138
|
-
# `Ratonvirus::Scanner::Eicar.new`. The user may also provide a second
|
139
|
-
# argument to the setter method e.g. like
|
140
|
-
# `Ratonvirus.scanner = :eicar, {conf: 'option'}`, in which case these
|
141
|
-
# both arguments are provided in this argument as an array.
|
142
|
-
def set_backend(backend_type, backend_cls, backend_value)
|
143
|
-
base_class = backend_class(backend_cls, "Base")
|
117
|
+
@#{backend_type}_defs[:klass].new(
|
118
|
+
@#{backend_type}_defs[:config]
|
119
|
+
)
|
120
|
+
end
|
121
|
+
private_class_method :create_#{backend_type}
|
122
|
+
CODE
|
123
|
+
end
|
144
124
|
|
145
|
-
|
146
|
-
|
147
|
-
|
125
|
+
# Sets the backend to local variables for the backend initialization.
|
126
|
+
# The goal of this method is to get the following configuration set to
|
127
|
+
# local `@x_defs` variable, where 'x' is the type of backend.
|
128
|
+
#
|
129
|
+
# For example, for a backend with type "scanner", this would be
|
130
|
+
# @scanner_defs.
|
131
|
+
#
|
132
|
+
# The first argument, "backend_type" is the type of backend we are
|
133
|
+
# configuring, e.g. `:scanner`.
|
134
|
+
#
|
135
|
+
# The second argument "backend_cls" is the backend subclass that is
|
136
|
+
# used in the module's namespace, e.g. "Scanner". This would refer to
|
137
|
+
# subclasses `Ratonvirus::Scanner::...`.
|
138
|
+
#
|
139
|
+
# The third argument "backend_value" is the actual value the user
|
140
|
+
# provided for the setter method, e.g. `:eicar` or
|
141
|
+
# `Ratonvirus::Scanner::Eicar.new`. The user may also provide a second
|
142
|
+
# argument to the setter method e.g. like
|
143
|
+
# `Ratonvirus.scanner = :eicar, {conf: 'option'}`, in which case these
|
144
|
+
# both arguments are provided in this argument as an array.
|
145
|
+
def set_backend(backend_type, backend_cls, backend_value)
|
146
|
+
base_class = backend_class(backend_cls, "Base")
|
148
147
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
config = backend_value.config
|
153
|
-
else
|
154
|
-
if backend_value.is_a?(Array)
|
155
|
-
subtype = backend_value.shift
|
156
|
-
config = backend_value.shift || {}
|
148
|
+
if backend_value.is_a?(base_class)
|
149
|
+
# Set the instance
|
150
|
+
instance_variable_set(:"@#{backend_type}", backend_value)
|
157
151
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
else
|
167
|
-
raise InvalidError.new("Invalid #{backend_type} provided!")
|
168
|
-
end
|
152
|
+
# Store the class (type) and config for storing them below to local
|
153
|
+
# variable in case it needs to be re-initialized at some point.
|
154
|
+
subtype = backend_value.class
|
155
|
+
config = backend_value.config
|
156
|
+
else
|
157
|
+
if backend_value.is_a?(Array)
|
158
|
+
subtype = backend_value.shift
|
159
|
+
config = backend_value.shift || {}
|
169
160
|
|
170
|
-
#
|
171
|
-
|
161
|
+
raise InvalidError, "Invalid #{backend_type} type: #{subtype}" unless subtype.is_a?(Symbol)
|
162
|
+
elsif backend_value.is_a?(Symbol)
|
163
|
+
subtype = backend_value
|
164
|
+
config = {}
|
165
|
+
else
|
166
|
+
raise InvalidError, "Invalid #{backend_type} provided!"
|
172
167
|
end
|
173
168
|
|
174
|
-
|
175
|
-
|
176
|
-
config: config,
|
177
|
-
})
|
169
|
+
# Destroy the current one
|
170
|
+
send(:"destroy_#{backend_type}")
|
178
171
|
end
|
172
|
+
|
173
|
+
instance_variable_set(
|
174
|
+
:"@#{backend_type}_defs",
|
175
|
+
klass: backend_class(backend_cls, subtype),
|
176
|
+
config: config
|
177
|
+
)
|
178
|
+
end
|
179
179
|
end
|
180
180
|
end
|
181
181
|
end
|
data/lib/ratonvirus/version.rb
CHANGED
data/lib/ratonvirus.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "fileutils"
|
2
4
|
require "active_support"
|
3
5
|
require "digest"
|
@@ -42,7 +44,7 @@ module Ratonvirus
|
|
42
44
|
#
|
43
45
|
# Usage (destroy):
|
44
46
|
# Ratonvirus.destroy_scanner
|
45
|
-
define_backend :scanner,
|
47
|
+
define_backend :scanner, "Scanner"
|
46
48
|
|
47
49
|
# Usage (set):
|
48
50
|
# Ratonvirus.storage = :active_storage
|
@@ -55,19 +57,19 @@ module Ratonvirus
|
|
55
57
|
#
|
56
58
|
# Usage (destroy):
|
57
59
|
# Ratonvirus.destroy_storage
|
58
|
-
define_backend :storage,
|
60
|
+
define_backend :storage, "Storage"
|
59
61
|
|
60
62
|
# Resets Ratonvirus to its initial state and configuration
|
61
63
|
def self.reset
|
62
64
|
# Default addons
|
63
65
|
@addons = [
|
64
66
|
ActiveSupport::Inflector.constantize(
|
65
|
-
"#{
|
66
|
-
)
|
67
|
+
"#{name}::Scanner::Addon::RemoveInfected"
|
68
|
+
)
|
67
69
|
]
|
68
70
|
|
69
|
-
|
70
|
-
|
71
|
+
destroy_scanner
|
72
|
+
destroy_storage
|
71
73
|
end
|
72
74
|
|
73
75
|
def self.addons
|
@@ -77,15 +79,13 @@ module Ratonvirus
|
|
77
79
|
def self.addons=(addons)
|
78
80
|
@addons = []
|
79
81
|
addons.each do |addon|
|
80
|
-
|
82
|
+
add_addon addon
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
84
86
|
def self.add_addon(addon)
|
85
87
|
addon_cls = addon_class(addon)
|
86
|
-
unless @addons.include?(addon_cls)
|
87
|
-
@addons << addon_cls
|
88
|
-
end
|
88
|
+
@addons << addon_cls unless @addons.include?(addon_cls)
|
89
89
|
end
|
90
90
|
|
91
91
|
def self.remove_addon(addon)
|
@@ -99,7 +99,7 @@ module Ratonvirus
|
|
99
99
|
|
100
100
|
subclass = ActiveSupport::Inflector.camelize(type.to_s)
|
101
101
|
ActiveSupport::Inflector.constantize(
|
102
|
-
"#{
|
102
|
+
"#{name}::Scanner::Addon::#{subclass}"
|
103
103
|
)
|
104
104
|
end
|
105
105
|
private_class_method :addon_class
|
data/lib/tasks/ratonvirus.rake
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :ratonvirus do
|
2
4
|
desc "Tests if the antivirus scanner is available and properly configured"
|
3
5
|
task test: :environment do
|
@@ -9,7 +11,7 @@ namespace :ratonvirus do
|
|
9
11
|
puts ""
|
10
12
|
puts "Please refer to Ratonvirus documentation for proper configuration."
|
11
13
|
end
|
12
|
-
rescue
|
14
|
+
rescue StandardError
|
13
15
|
puts "Ratonvirus scanner is not configured."
|
14
16
|
puts ""
|
15
17
|
puts "Please refer to Ratonvirus documentation for proper configuration."
|
@@ -18,7 +20,7 @@ namespace :ratonvirus do
|
|
18
20
|
|
19
21
|
desc "Scans the given file through the antivirus scanner"
|
20
22
|
task scan: :environment do |t, args|
|
21
|
-
if args.extras.
|
23
|
+
if args.extras.empty?
|
22
24
|
puts "No files given."
|
23
25
|
puts "Usage:"
|
24
26
|
puts " #{t.name}[/path/to/first/file.pdf,/path/to/second/file.pdf]"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ratonvirus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Antti Hukkanen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -122,6 +122,34 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '1.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.64.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.64.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-rspec
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.32'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.32'
|
125
153
|
description: Adds antivirus check capability for Rails applications.
|
126
154
|
email:
|
127
155
|
- antti.hukkanen@mainiotech.fi
|
@@ -171,8 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
199
|
- !ruby/object:Gem::Version
|
172
200
|
version: '0'
|
173
201
|
requirements: []
|
174
|
-
|
175
|
-
rubygems_version: 2.7.7
|
202
|
+
rubygems_version: 3.0.3
|
176
203
|
signing_key:
|
177
204
|
specification_version: 4
|
178
205
|
summary: Provides antivirus checks for Rails.
|