Skip to content

Commit

Permalink
Added ClamAV integration (#5640)
Browse files Browse the repository at this point in the history
  • Loading branch information
farhatahmad authored Dec 29, 2023
1 parent ede0874 commit 3302345
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 16 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ gem 'aws-sdk-s3', require: false
gem 'bcrypt', '~> 3.1.7'
gem 'bigbluebutton-api-ruby', '1.9.1'
gem 'bootsnap', require: false
gem 'clamby', '~> 1.6.10'
gem 'cssbundling-rails', '>= 1.3.3'
gem 'data_migrate', '>= 9.2.0'
gem 'dotenv-rails'
Expand Down
8 changes: 2 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ GEM
case_transform (0.2)
activesupport
childprocess (4.1.0)
clamby (1.6.10)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crack (0.4.5)
Expand Down Expand Up @@ -505,6 +506,7 @@ DEPENDENCIES
bigbluebutton-api-ruby (= 1.9.1)
bootsnap
capybara
clamby (~> 1.6.10)
cssbundling-rails (>= 1.3.3)
data_migrate (>= 9.2.0)
debug
Expand Down Expand Up @@ -545,9 +547,3 @@ DEPENDENCIES
web-console (>= 4.2.1)
webdrivers
webmock

RUBY VERSION
ruby 3.1.0p0

BUNDLED WITH
2.3.9
1 change: 1 addition & 0 deletions app/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
"file_size_too_large": "The file size is too large.",
"file_upload_error": "The file can't be uploaded.",
"signin_required": "You must be signed in to access this page.",
"malware_detected": "Malware Detected! The file you uploaded may contain malware. Please check your file and try again.",
"roles": {
"role_assigned": "This role can't be deleted because it is assigned to at least one user."
},
Expand Down
5 changes: 1 addition & 4 deletions app/controllers/api/v1/admin/site_settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ def update
site_setting.update(value: params[:site_setting][:value].to_s)
end

unless update
return render_error status: :bad_request,
errors: Rails.configuration.custom_error_msgs[:record_invalid]
end
return render_error status: :bad_request, errors: site_setting.errors.to_a unless update

render_data status: :ok
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def update
create_default_room(user)
render_data status: :ok
else
render_error errors: Rails.configuration.custom_error_msgs[:record_invalid]
render_error errors: user.errors.to_a
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export default function useUpdateSiteSetting(name) {
handleSuccess();
},
onError: (error) => {
handleError(error, t, toast);
if (error.response.data.errors.includes('Image MalwareDetected')) {
toast.error(t('toast.error.malware_detected'));
} else {
handleError(error, t, toast);
}
},
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export default function useUploadPresentation(friendlyId) {
toast.success(t('toast.success.room.presentation_updated'));
},
onError: (error) => {
handleError(error, t, toast);
if (error.response.data.errors.includes('Presentation MalwareDetected')) {
toast.error(t('toast.error.malware_detected'));
} else {
handleError(error, t, toast);
}
},
});

Expand Down
8 changes: 6 additions & 2 deletions app/javascript/hooks/mutations/users/useCreateAvatar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@ export default function useCreateAvatar(currentUser) {
queryClient.invalidateQueries('getUser');
toast.success(t('toast.success.user.avatar_updated'));
},
onError: () => {
toast.error(t('toast.error.problem_completing_action'));
onError: (error) => {
if (error.response.data.errors.includes('Avatar MalwareDetected')) {
toast.error(t('toast.error.malware_detected'));
} else {
toast.error(t('toast.error.problem_completing_action'));
}
},
},
);
Expand Down
4 changes: 4 additions & 0 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
self.implicit_order_column = 'created_at'

def virus_scan?
ENV.fetch('CLAMAV_SCANNING', 'false') == 'true'
end
end
13 changes: 13 additions & 0 deletions app/models/room.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Room < ApplicationRecord
validates :recordings_processing, numericality: { only_integer: true, greater_than_or_equal_to: 0 }

before_validation :set_friendly_id, :set_meeting_id, on: :create
before_save :scan_presentation_for_virus

after_create :create_meeting_options

attr_accessor :shared, :active, :participants
Expand Down Expand Up @@ -99,4 +101,15 @@ def set_meeting_id
rescue StandardError
retry
end

def scan_presentation_for_virus
return if !virus_scan? || !attachment_changes['presentation']

path = attachment_changes['presentation']&.attachable&.tempfile&.path

return true if Clamby.safe?(path)

errors.add(:presentation, 'MalwareDetected')
throw :abort
end
end
15 changes: 15 additions & 0 deletions app/models/site_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,19 @@ class SiteSetting < ApplicationRecord
validates :image,
content_type: Rails.configuration.uploads[:images][:formats],
size: { less_than: Rails.configuration.uploads[:images][:max_size] }

before_save :scan_image_for_virus

private

def scan_image_for_virus
return if !virus_scan? || !attachment_changes['image']

path = attachment_changes['image']&.attachable&.tempfile&.path

return true if Clamby.safe?(path)

errors.add(:image, 'MalwareDetected')
throw :abort
end
end
14 changes: 14 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class User < ApplicationRecord
validate :check_user_role_provider, if: :role_changed?

before_validation :set_session_token, on: :create
before_save :scan_avatar_for_virus

scope :with_provider, ->(current_provider) { where(provider: current_provider) }

Expand Down Expand Up @@ -214,4 +215,17 @@ def check_user_role_provider

errors.add(:user_provider, 'has to be the same as the Role provider') if provider != role.provider
end

private

def scan_avatar_for_virus
return if !virus_scan? || !attachment_changes['avatar']

path = attachment_changes['avatar']&.attachable&.tempfile&.path

return true if Clamby.safe?(path)

errors.add(:avatar, 'MalwareDetected')
throw :abort
end
end
24 changes: 24 additions & 0 deletions config/initializers/clamby.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
#
# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Greenlight; if not, see <http://www.gnu.org/licenses/>.

# frozen_string_literal: true

Clamby.configure({
check: ENV.fetch('CLAMAV_SCANNING', 'false') == 'true',
daemonize: ENV.fetch('CLAMAV_DAEMONIZE', 'true') == 'true',
fdpass: ENV.fetch('CLAMAV_DAEMONIZE', 'true') == 'true',
config_file: ENV.fetch('CLAMAV_CONFIG', nil)
})
2 changes: 1 addition & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,10 @@ LOG_LEVEL=info
# RAILS_LOG_REMOTE_NAME=xxx.papertrailapp.com
# RAILS_LOG_REMOTE_PORT=99999
# RAILS_LOG_REMOTE_TAG=greenlight-v3

## ClamAV Virus Scanning
# If you have ClamAV installed on the same machine as your Greenlight deployment, you can enable automatic virus scanning
# for presentations, avatars, and BrandingImage. If a malicious file is detected, the user will be informed and asked
# to check their file.
#CLAMAV_SCANNING=true
#CLAMAV_DAEMONIZE=true
36 changes: 36 additions & 0 deletions spec/models/room_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@
end
end

describe 'before_save' do
describe '#scan_presentation_for_virus' do
let(:room) { create(:room) }

before do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(true)
end

it 'makes a call to ClamAV if CLAMAV_SCANNING=true' do
expect(Clamby).to receive(:safe?)

room.presentation.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end

it 'adds an error if the file is not safe' do
allow(Clamby).to receive(:safe?).and_return(false)
room.presentation.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
expect(room.errors[:presentation]).to eq(['MalwareDetected'])
end

it 'does not makes a call to ClamAV if the image is not changing' do
expect(Clamby).not_to receive(:safe?)

room.update(name: 'New Name')
end

it 'does not makes a call to ClamAV if CLAMAV_SCANNING=false' do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(false)

expect(Clamby).not_to receive(:safe?)

room.presentation.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end
end
end

context 'after_create' do
describe 'create_meeting_options' do
let!(:viewer_access_code) { create(:meeting_option, name: 'glViewerAccessCode', default_value: '') }
Expand Down
36 changes: 36 additions & 0 deletions spec/models/site_setting_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,40 @@
end
end
end

describe 'before_save' do
describe '#scan_image_for_virus' do
let(:site_setting) { create(:site_setting) }

before do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(true)
end

it 'makes a call to ClamAV if CLAMAV_SCANNING=true' do
expect(Clamby).to receive(:safe?)

site_setting.image.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end

it 'adds an error if the file is not safe' do
allow(Clamby).to receive(:safe?).and_return(false)
site_setting.image.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
expect(site_setting.errors[:image]).to eq(['MalwareDetected'])
end

it 'does not makes a call to ClamAV if the image is not changing' do
expect(Clamby).not_to receive(:safe?)

site_setting.update(provider: 'New Provider')
end

it 'does not makes a call to ClamAV if CLAMAV_SCANNING=false' do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(false)

expect(Clamby).not_to receive(:safe?)

site_setting.image.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end
end
end
end
34 changes: 34 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,38 @@
expect(user.errors[:user_provider]).not_to be_empty
end
end

describe '#scan_avatar_for_virus' do
let(:user) { create(:user) }

before do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(true)
end

it 'makes a call to ClamAV if CLAMAV_SCANNING=true' do
expect(Clamby).to receive(:safe?)

user.avatar.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end

it 'adds an error if the file is not safe' do
allow(Clamby).to receive(:safe?).and_return(false)
user.avatar.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
expect(user.errors[:avatar]).to eq(['MalwareDetected'])
end

it 'does not makes a call to ClamAV if the image is not changing' do
expect(Clamby).not_to receive(:safe?)

user.update(name: 'New Name')
end

it 'does not makes a call to ClamAV if CLAMAV_SCANNING=false' do
allow_any_instance_of(described_class).to receive(:virus_scan?).and_return(false)

expect(Clamby).not_to receive(:safe?)

user.avatar.attach(fixture_file_upload(file_fixture('default-avatar.png'), 'image/png'))
end
end
end

0 comments on commit 3302345

Please sign in to comment.