ratonvirus 0.1.0

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.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratonvirus
4
+ module Storage
5
+ class Carrierwave < Base
6
+ def changed?(record, attribute)
7
+ record.public_send :"#{attribute}_changed?"
8
+ end
9
+
10
+ def accept?(resource)
11
+ if resource.is_a?(Array)
12
+ resource.all? { |subr| subr.is_a?(::CarrierWave::Uploader::Base) }
13
+ else
14
+ resource.is_a?(::CarrierWave::Uploader::Base)
15
+ end
16
+ end
17
+
18
+ def asset_path(asset)
19
+ return unless block_given?
20
+ return if asset.nil?
21
+ return if asset.file.nil?
22
+
23
+ yield asset.file.path
24
+ end
25
+
26
+ def asset_remove(asset)
27
+ path = asset.file.path
28
+ result = asset.remove!
29
+
30
+ # Remove the temp cache dir if it exists
31
+ dir = File.dirname(path)
32
+ FileUtils.remove_dir(dir) if File.directory?(dir)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ module Ratonvirus
2
+ module Storage
3
+ class Filepath < Base
4
+ def changed?(record, attribute)
5
+ if record.respond_to? :"#{attribute}_changed?"
6
+ return record.public_send :"#{attribute}_changed?"
7
+ end
8
+
9
+ # Some backends do not implement the `attribute_changed?` methods for
10
+ # the file resources. In that case our best guess is to check whether
11
+ # the whole record has changed.
12
+ record.changed?
13
+ end
14
+
15
+ def accept?(resource)
16
+ if resource.is_a?(Array)
17
+ resource.all? { |r| r.is_a?(String) || r.is_a?(File) }
18
+ else
19
+ resource.is_a?(String) || resource.is_a?(File)
20
+ end
21
+ end
22
+
23
+ def asset_path(asset, &block)
24
+ return unless block_given?
25
+
26
+ return unless asset
27
+ return if asset.empty?
28
+
29
+ if asset.respond_to?(:path)
30
+ # A file asset that responds to path (e.g. default `File`
31
+ # object).
32
+ asset_path(asset.path, &block)
33
+
34
+ return
35
+ end
36
+
37
+ # Plain file path string provided as resource
38
+ yield asset
39
+ end
40
+
41
+ def asset_remove(asset)
42
+ FileUtils.remove_file(asset) if File.file?(asset)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ module Ratonvirus
2
+ module Storage
3
+ # Multi storage allows the developers to configure multiple storage backends
4
+ # for the application at the same time. For instance, in case the scanner is
5
+ # used for both: scanning the Active Storage resources as well as scanning
6
+ # file paths, they are handled with separate storages.
7
+ #
8
+ # To configure the Multi-storage with two backends, use the following:
9
+ # Ratonvirus.storage = :multi, {storages: [:filepath, :active_storage]}
10
+ class Multi < Base
11
+ # Setup the @storages array with the initialized storage instances.
12
+ def setup
13
+ @storages = []
14
+
15
+ if config[:storages].is_a?(Array)
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
23
+
24
+ cls = Ratonvirus.backend_class('Storage', type)
25
+ @storages << cls.new(storage_config || {})
26
+ end
27
+ end
28
+ end
29
+
30
+ # Processing of the resource is handled by the first storage in the list
31
+ # that returns `true` for `accept?(resource)`. Any consequent storages are
32
+ # skipped.
33
+ def process(resource, &block)
34
+ return unless block_given?
35
+
36
+ storage_for(resource) do |storage|
37
+ storage.process(resource, &block)
38
+ end
39
+ end
40
+
41
+ # Fetch the resource from the record using the attribute and check if any
42
+ # storages accept that resource. If an accepting storage is found, only
43
+ # check `changed?` against that storage. Otherwise, call the `changed?`
44
+ # method passing both given parameters for all storages in order and
45
+ # return in case one of them reports the resource to be changed.
46
+ def changed?(record, attribute)
47
+ resource = record.public_send(attribute)
48
+
49
+ storage_for(resource) do |storage|
50
+ return storage.changed?(record, attribute)
51
+ end
52
+
53
+ false
54
+ end
55
+
56
+ # Check if any of the storages accept the resource.
57
+ def accept?(resource)
58
+ storage_for(resource) do |storage|
59
+ return true
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ private
66
+ # Iterates through the @storages array and returns the first storage
67
+ # that accepts the resource.
68
+ def storage_for(resource)
69
+ @storages.each do |storage|
70
+ if storage.accept?(resource)
71
+ yield storage
72
+ return
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,181 @@
1
+ module Ratonvirus
2
+ module Support
3
+ # The backend implementation allows us to set different backends on the main
4
+ # Ratonvirus configuration, e.g. scanner and storage backends. This makes
5
+ # the library agnostic of the actual implementation of these both and allows
6
+ # the developer to configure
7
+ #
8
+ # The solution is a bit hacky monkey patch type of solution as it adds code
9
+ # to the underlying implementation through class_eval. The reason for this
10
+ # is to define arbitrary getter, setter and destroye methods that are nicer
11
+ # to use for the user. Wrapping this functionality to its own module
12
+ # makes the resulting code less prone to errors as all of the backends are
13
+ # defined exactly the same way.
14
+ #
15
+ # Modifying this may be tough, so be sure to test properly in case you make
16
+ # any modifications.
17
+ module Backend
18
+ # First argument "backend_cls":
19
+ # The subclass that refers to the backend's namespace, e.g.
20
+ # `"Scanner"`.
21
+ #
22
+ # Second argument "backend_type":
23
+ # The backend type in the given namespace, e.g. `:eicar`
24
+ #
25
+ # The returned result will be e.g.
26
+ # Ratonvirus::Scanner::Eicar
27
+ # Ratonvirus::Storage::ActiveStorage
28
+ def backend_class(backend_cls, backend_type)
29
+ return backend_type if backend_type.is_a?(Class)
30
+
31
+ subclass = ActiveSupport::Inflector.camelize(backend_type.to_s)
32
+ ActiveSupport::Inflector.constantize(
33
+ "#{self.name}::#{backend_cls}::#{subclass}"
34
+ )
35
+ end
36
+
37
+ 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
+
92
+ # Setter for #{backend_type}
93
+ def self.#{backend_type}=(#{backend_type}_value)
94
+ set_backend(
95
+ :#{backend_type},
96
+ "#{backend_subclass}",
97
+ #{backend_type}_value
98
+ )
99
+ end
100
+
101
+ # Destroys the currently active #{backend_type}.
102
+ # The #{backend_type} is re-initialized when the getter is called.
103
+ def self.destroy_#{backend_type}
104
+ @#{backend_type} = nil
105
+ end
106
+
107
+ # Creates a new backend instance
108
+ # private
109
+ def self.create_#{backend_type}
110
+ if @#{backend_type}_defs.nil?
111
+ raise NotDefinedError.new("#{backend_subclass} not defined!")
112
+ end
113
+
114
+ @#{backend_type}_defs[:klass].new(
115
+ @#{backend_type}_defs[:config]
116
+ )
117
+ end
118
+ private_class_method :create_#{backend_type}
119
+ CODE
120
+ end
121
+
122
+ # Sets the backend to local variables for the backend initialization.
123
+ # The goal of this method is to get the following configuration set to
124
+ # local `@x_defs` variable, where 'x' is the type of backend.
125
+ #
126
+ # For example, for a backend with type "scanner", this would be
127
+ # @scanner_defs.
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")
144
+
145
+ if backend_value.is_a?(base_class)
146
+ # Set the instance
147
+ instance_variable_set(:"@#{backend_type}", backend_value)
148
+
149
+ # Store the class (type) and config for storing them below to local
150
+ # variable in case it needs to be re-initialized at some point.
151
+ subtype = backend_value.class
152
+ config = backend_value.config
153
+ else
154
+ if backend_value.is_a?(Array)
155
+ subtype = backend_value.shift
156
+ config = backend_value.shift || {}
157
+
158
+ unless subtype.is_a?(Symbol)
159
+ raise InvalidError.new(
160
+ "Invalid #{backend_type} type: #{subtype}"
161
+ )
162
+ end
163
+ elsif backend_value.is_a?(Symbol)
164
+ subtype = backend_value
165
+ config = {}
166
+ else
167
+ raise InvalidError.new("Invalid #{backend_type} provided!")
168
+ end
169
+
170
+ # Destroy the current one
171
+ send(:"destroy_#{backend_type}")
172
+ end
173
+
174
+ instance_variable_set(:"@#{backend_type}_defs", {
175
+ klass: backend_class(backend_cls, subtype),
176
+ config: config,
177
+ })
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ratonvirus
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,40 @@
1
+ namespace :ratonvirus do
2
+ desc "Tests if the antivirus scanner is available and properly configured"
3
+ task test: :environment do
4
+ begin
5
+ if Ratonvirus.scanner.available?
6
+ puts "Ratonvirus correctly configured."
7
+ else
8
+ puts "Ratonvirus scanner is not available!"
9
+ puts ""
10
+ puts "Please refer to Ratonvirus documentation for proper configuration."
11
+ end
12
+ rescue
13
+ puts "Ratonvirus scanner is not configured."
14
+ puts ""
15
+ puts "Please refer to Ratonvirus documentation for proper configuration."
16
+ end
17
+ end
18
+
19
+ desc "Scans the given file through the antivirus scanner"
20
+ task scan: :environment do |t, args|
21
+ if args.extras.length < 1
22
+ puts "No files given."
23
+ puts "Usage:"
24
+ puts " #{t.name}[/path/to/first/file.pdf,/path/to/second/file.pdf]"
25
+ next
26
+ end
27
+
28
+ args.extras.each do |path|
29
+ if File.file?(path)
30
+ if Ratonvirus.scanner.virus?(path)
31
+ puts "Detected a virus at: #{path}"
32
+ else
33
+ puts "Clean file at: #{path}"
34
+ end
35
+ else
36
+ puts "File does not exist at: #{path}"
37
+ end
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ratonvirus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Antti Hukkanen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.16.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.16.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: activemodel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activestorage
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: carrierwave
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.2'
125
+ description: Adds antivirus check capability for Rails applications.
126
+ email:
127
+ - antti.hukkanen@mainiotech.fi
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - CHANGELOG.md
133
+ - LICENSE
134
+ - README.md
135
+ - app/validators/antivirus_validator.rb
136
+ - config/locales/en.yml
137
+ - config/locales/fi.yml
138
+ - config/locales/sv.yml
139
+ - lib/ratonvirus.rb
140
+ - lib/ratonvirus/engine.rb
141
+ - lib/ratonvirus/error.rb
142
+ - lib/ratonvirus/processable.rb
143
+ - lib/ratonvirus/scanner/addon/remove_infected.rb
144
+ - lib/ratonvirus/scanner/base.rb
145
+ - lib/ratonvirus/scanner/eicar.rb
146
+ - lib/ratonvirus/scanner/support/callbacks.rb
147
+ - lib/ratonvirus/storage/active_storage.rb
148
+ - lib/ratonvirus/storage/base.rb
149
+ - lib/ratonvirus/storage/carrierwave.rb
150
+ - lib/ratonvirus/storage/filepath.rb
151
+ - lib/ratonvirus/storage/multi.rb
152
+ - lib/ratonvirus/support/backend.rb
153
+ - lib/ratonvirus/version.rb
154
+ - lib/tasks/ratonvirus.rake
155
+ homepage: https://github.com/mainio/ratonvirus
156
+ licenses:
157
+ - MIT
158
+ metadata: {}
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 2.7.7
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: Provides antivirus checks for Rails.
179
+ test_files: []