ratonvirus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []