ratonvirus 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|