Automatically rescan attachments

This commit is contained in:
Oliver Günther
2024-02-18 21:12:33 +01:00
parent 130efad25e
commit d5397385a0
11 changed files with 279 additions and 27 deletions
@@ -31,8 +31,7 @@ module Admin::Settings
menu_item :settings_attachments
before_action :require_ee
before_action :check_clamav, only: %i[update], if: -> { scan_will_enable? }
before_action :mark_unscanned_attachments, if: -> { scan_will_enable? }
before_action :check_clamav, only: %i[update], if: -> { scan_enabled? }
def default_breadcrumb
t('settings.antivirus.title')
@@ -73,19 +72,34 @@ module Admin::Settings
redirect_to action: :show
end
def scan_will_enable?
Setting.antivirus_scan_mode == :disabled && params.dig(:settings, :antivirus_scan_mode) != 'disabled'
def scan_enabled?
Setting.antivirus_scan_mode != :disabled || params.dig(:settings, :antivirus_scan_mode) != 'disabled'
end
def success_callback(_call)
if Setting.antivirus_scan_mode == :disabled && Attachment.status_quarantined.any?
flash[:info] = t('settings.antivirus.remaining_quarantined_files_html',
link: helpers.link_to(t('antivirus_scan.quarantined_attachments.title'), admin_quarantined_attachments_path),
file_count: t(:label_x_files, count: Attachment.status_quarantined.count))
redirect_to action: :show
remaining_quarantine_warning
elsif scan_enabled?
rescan_files
else
super
end
end
def rescan_files
flash[:info] = t('settings.antivirus.remaining_rescanned_files',
file_count: t(:label_x_files, count: Attachment.status_uploaded.count))
Attachment.status_uploaded.update_all(status: :rescan)
job = Attachments::VirusRescanJob.perform_later
redirect_to job_status_path(job.job_id)
end
def remaining_quarantine_warning
flash[:info] = t('settings.antivirus.remaining_quarantined_files_html',
link: helpers.link_to(t('antivirus_scan.quarantined_attachments.title'), admin_quarantined_attachments_path),
file_count: t(:label_x_files, count: Attachment.status_quarantined.count))
redirect_to action: :show
end
end
end
+2 -1
View File
@@ -33,7 +33,8 @@ class Attachment < ApplicationRecord
uploaded: 0,
prepared: 1,
scanned: 2,
quarantined: 3
quarantined: 3,
rescan: 4
}.freeze, _prefix: true
belongs_to :container, polymorphic: true
@@ -1,8 +0,0 @@
<%=
render(Primer::Alpha::Dialog.new(title: t('settings.antivirus.unscanned.title'))) do |d|
d.with_header(variant: :large)
d.with_body do
"You have not scanned #{attachments.pluck(:filename)}"
end
end
%>
@@ -28,10 +28,6 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= toolbar title: t(:'settings.antivirus.title') %>
<% if true || (Setting::VirusScanning.enabled? && @unscanned_atachments&.exists?) %>
<%= render partial: 'unscanned_dialog', locals: { attachments: Attachment.status_uploaded } %>
<% end %>
<%= styled_form_tag(
admin_settings_virus_scanning_path,
method: :patch,
@@ -0,0 +1,68 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Attachments
class VirusRescanJob < VirusScanJob
queue_with_priority :low
def perform
return unless Setting::VirusScanning.enabled?
User.execute_as(User.system) do
OpenProject::Mutex.with_advisory_lock(Attachment, 'virus_rescan') do
Attachment.status_rescan.find_each do |attachment|
scan_attachment(attachment)
rescue StandardError => e
Rails.logger.error "Failed to rescan #{attachment.id} for viruses: #{e.message}. Attachment will remain accessible."
end
end
end
redirect_status
end
def redirect_status
path = ApplicationController.helpers.admin_quarantined_attachments_path
payload = redirect_payload(path)
html = I18n.t('settings.antivirus.remaining_scan_complete_html',
file_count: I18n.t(:label_x_files, count: Attachment.status_quarantined.count))
upsert_status(
status: :success,
payload: payload.merge(html:)
)
end
def store_status?
true
end
def updates_own_status?
true
end
end
end
@@ -52,6 +52,7 @@ module Attachments
private
def scan_attachment(attachment)
Rails.logger.debug { "Scanning file #{attachment.id} for viruses." }
service = Attachments::ClamAVService.new
response = service.scan(attachment)
case response
+9 -6
View File
@@ -3001,14 +3001,17 @@ en:
antivirus:
title: "Virus scanning"
clamav_ping_failed: "Failed to connect the the ClamAV daemon. Double-check the configuration and try again."
unscanned:
title: 'Unscanned attachments'
remaining_quarantined_files_html: >
Virus scanning has been disbled. %{file_count} remain in quarantine.
To review quarantined files, please visit this link: %{link}
remaining_scan_complete_html: >
Remaining files have been scanned. There are %{file_count} in quarantine.
You are being redirected to the quarantine page. Use this page to delete or override quarantined files.
remaining_rescanned_files: >
With virus scanning now active, there are %{file_count} that need to be rescanned.
This process has been scheduled in the background. The files will remain accessible during the scan.
upsale:
description: "Ensure uploaded files in OpenProject are scanned for viruses before being accessible by other users."
remaining_quarantined_files_html: >
You have successfully disabled virus scanning.
There are however still %{file_count} in quarantine.
Visit %{link} to clean them up."
actions:
delete: 'Delete the file'
quarantine: 'Quarantine the file'
@@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe Attachments::VirusRescanJob,
with_ee: %i[virus_scanning],
with_settings: { antivirus_scan_mode: :clamav_socket } do
let!(:attachment1) { create(:attachment, status: :uploaded) }
let!(:attachment2) { create(:attachment, status: :rescan) }
let!(:attachment3) { create(:attachment, status: :rescan) }
let(:client_double) { instance_double(ClamAV::Client) }
subject { described_class.perform_now }
before do
allow(ClamAV::Client).to receive(:new).and_return(client_double)
end
describe '#perform' do
let(:response) { ClamAV::SuccessResponse.new('wat') }
before do
allow(client_double)
.to receive(:execute).with(instance_of(ClamAV::Commands::InstreamCommand))
.and_return(response)
end
it 'updates the attachments' do
subject
expect(attachment1.reload).to be_status_uploaded
expect(attachment2.reload).to be_status_scanned
expect(attachment3.reload).to be_status_scanned
end
end
end
@@ -1,6 +1,7 @@
require 'rails_helper'
RSpec.describe Attachments::VirusScanJob,
with_ee: %i[virus_scanning],
with_settings: { antivirus_scan_mode: :clamav_socket } do
let(:attachment_status) { :uploaded }
let(:attachment) { build_stubbed(:attachment, status: attachment_status) }
@@ -0,0 +1,140 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
RSpec.describe 'Attachments virus scanning',
:skip_csrf,
type: :rails_request do
shared_let(:admin) { create(:admin) }
let(:service) { instance_double(Attachments::ClamAVService) }
before do
login_as admin
allow(::Attachments::ClamAVService)
.to receive(:new)
.and_return(service)
allow(service).to receive(:ping)
end
describe 'enabling virus scanning',
with_ee: %i[virus_scanning] do
subject do
patch '/admin/settings/virus_scanning',
params: {
settings: {
antivirus_scan_mode: 'clamav_socket'
}
}
response
end
it 'shows an error if ClamAV cannot be reached' do
allow(service).to receive(:ping).and_raise(Errno::ECONNREFUSED)
expect(subject).to be_redirect
follow_redirect!
expect(response.body).to have_text I18n.t('settings.antivirus.clamav_ping_failed')
expect(Setting.antivirus_scan_mode).to eq(:disabled)
end
it 'shows no error if ClamAV can be reached' do
expect(subject).to be_redirect
follow_redirect!
expect(response.body).not_to have_text I18n.t('settings.antivirus.clamav_ping_failed')
expect(Setting.antivirus_scan_mode).to eq(:clamav_socket)
end
end
describe 'rescanning uploaded files',
with_ee: %i[virus_scanning] do
shared_let(:attachment) { create(:attachment, status: :uploaded) }
it 'triggers rescanning of the uploaded files' do
patch '/admin/settings/virus_scanning',
params: {
settings: {
antivirus_scan_mode: 'clamav_socket'
}
}
expect(response).to be_redirect
follow_redirect!
expect(response.body).to have_text 'This process has been scheduled in the background'
expect(Setting.antivirus_scan_mode).to eq(:clamav_socket)
expect(attachment.reload).to be_status_rescan
expect(Attachments::VirusRescanJob)
.to have_been_enqueued
end
end
describe 'disabling virus scanning',
with_ee: %i[virus_scanning] do
shared_let(:attachment) { create(:attachment, status: :quarantined) }
it 'shows no warning if there are no quarantined files' do
attachment.destroy!
patch '/admin/settings/virus_scanning',
params: {
settings: {
antivirus_scan_mode: 'disabled'
}
}
expect(response).to be_redirect
follow_redirect!
expect(response.body).not_to have_text 'remain in quarantine.'
expect(Setting.antivirus_scan_mode).to eq(:disabled)
end
it 'shows a warning if there are still quarantined files' do
patch '/admin/settings/virus_scanning',
params: {
settings: {
antivirus_scan_mode: 'disabled'
}
}
expect(response).to be_redirect
follow_redirect!
expect(response.body).to have_text '1 file remain in quarantine.'
expect(Setting.antivirus_scan_mode).to eq(:disabled)
end
end
describe 'without ee' do
it 'redirects to upsale' do
get '/admin/settings/virus_scanning'
expect(response.body).to have_text 'Virus scanning is an Enterprise add-on', normalize_ws: true
end
end
end
+1
View File
@@ -13,4 +13,5 @@ RSpec.configure do |config|
# If desired, we can use the Rails IntegrationTest request spec
# (more like a feature spec) with this type.
config.include RSpec::Rails::RequestExampleGroup, type: :rails_request
config.include Capybara::RSpecMatchers, type: :rails_request
end