Save virus scan status to blob metadata
This commit is contained in:
parent
7e8df41648
commit
f113d108c9
13 changed files with 143 additions and 92 deletions
|
@ -1,20 +0,0 @@
|
|||
class AntiVirusJob < ApplicationJob
|
||||
include ActiveStorage::Downloading
|
||||
|
||||
attr_reader :blob
|
||||
|
||||
def perform(virus_scan)
|
||||
@blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key)
|
||||
|
||||
if @blob.present?
|
||||
download_blob_to_tempfile do |file|
|
||||
if ClamavService.safe_file?(file.path)
|
||||
status = VirusScan.statuses.fetch(:safe)
|
||||
else
|
||||
status = VirusScan.statuses.fetch(:infected)
|
||||
end
|
||||
virus_scan.update(scanned_at: Time.zone.now, status: status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
10
app/jobs/virus_scanner_job.rb
Normal file
10
app/jobs/virus_scanner_job.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class VirusScannerJob < ApplicationJob
|
||||
def perform(blob)
|
||||
metadata = extract_metadata_via_virus_scanner(blob)
|
||||
blob.update!(metadata: blob.metadata.merge(metadata))
|
||||
end
|
||||
|
||||
def extract_metadata_via_virus_scanner(blob)
|
||||
ActiveStorage::VirusScanner.new(blob).metadata
|
||||
end
|
||||
end
|
46
app/lib/active_storage/virus_scanner.rb
Normal file
46
app/lib/active_storage/virus_scanner.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
class ActiveStorage::VirusScanner
|
||||
include ActiveStorage::Downloading
|
||||
|
||||
def initialize(blob)
|
||||
@blob = blob
|
||||
end
|
||||
|
||||
attr_reader :blob
|
||||
|
||||
PENDING = 'pending'
|
||||
INFECTED = 'infected'
|
||||
SAFE = 'safe'
|
||||
|
||||
def pending?
|
||||
blob.metadata[:virus_scan_result] == PENDING
|
||||
end
|
||||
|
||||
def infected?
|
||||
blob.metadata[:virus_scan_result] == INFECTED
|
||||
end
|
||||
|
||||
def safe?
|
||||
blob.metadata[:virus_scan_result] == SAFE
|
||||
end
|
||||
|
||||
def analyzed?
|
||||
blob.metadata[:virus_scan_result].present?
|
||||
end
|
||||
|
||||
def analyze_later
|
||||
if !analyzed?
|
||||
blob.update!(metadata: blob.metadata.merge(virus_scan_result: PENDING))
|
||||
VirusScannerJob.perform_later(blob)
|
||||
end
|
||||
end
|
||||
|
||||
def metadata
|
||||
download_blob_to_tempfile do |file|
|
||||
if ClamavService.safe_file?(file.path)
|
||||
{ virus_scan_result: SAFE, scanned_at: Time.zone.now }
|
||||
else
|
||||
{ virus_scan_result: INFECTED, scanned_at: Time.zone.now }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,5 @@
|
|||
class Avis < ApplicationRecord
|
||||
include EmailSanitizableConcern
|
||||
include VirusScanConcern
|
||||
|
||||
belongs_to :dossier, touch: true
|
||||
belongs_to :gestionnaire
|
||||
|
@ -22,9 +21,6 @@ class Avis < ApplicationRecord
|
|||
scope :by_latest, -> { order(updated_at: :desc) }
|
||||
scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) }
|
||||
|
||||
after_commit :create_avis_virus_scan
|
||||
after_initialize { add_virus_scan_on(self.piece_justificative_file) }
|
||||
|
||||
# The form allows subtmitting avis requests to several emails at once,
|
||||
# hence this virtual attribute.
|
||||
attr_accessor :emails
|
||||
|
@ -41,6 +37,27 @@ class Avis < ApplicationRecord
|
|||
Avis.find_by(id: avis_id)&.email == email
|
||||
end
|
||||
|
||||
# FIXME remove this after migrating virus_scan to blob metadata
|
||||
def virus_scan
|
||||
VirusScan.find_by(blob_key: piece_justificative_file.blob.key)
|
||||
end
|
||||
|
||||
def virus_scan_safe?
|
||||
virus_scan&.safe? || piece_justificative_file.virus_scanner.safe?
|
||||
end
|
||||
|
||||
def virus_scan_infected?
|
||||
virus_scan&.infected? || piece_justificative_file.virus_scanner.infected?
|
||||
end
|
||||
|
||||
def virus_scan_pending?
|
||||
virus_scan&.pending? || piece_justificative_file.virus_scanner.pending?
|
||||
end
|
||||
|
||||
def virus_scan_no_scan?
|
||||
virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_gestionnaire
|
||||
|
@ -54,8 +71,4 @@ class Avis < ApplicationRecord
|
|||
self.email = nil
|
||||
end
|
||||
end
|
||||
|
||||
def create_avis_virus_scan
|
||||
create_virus_scan(self.piece_justificative_file)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
class Champs::PieceJustificativeChamp < Champ
|
||||
after_commit :create_virus_scan
|
||||
|
||||
PIECE_JUSTIFICATIVE_FILE_MAX_SIZE = 200.megabytes
|
||||
|
||||
PIECE_JUSTIFICATIVE_FILE_ACCEPTED_FORMATS = [
|
||||
|
@ -48,20 +46,26 @@ class Champs::PieceJustificativeChamp < Champ
|
|||
errors
|
||||
end
|
||||
|
||||
# FIXME remove this after migrating virus_scan to blob metadata
|
||||
def virus_scan_safe?
|
||||
virus_scan&.safe? || piece_justificative_file.virus_scanner.safe?
|
||||
end
|
||||
|
||||
def virus_scan_infected?
|
||||
virus_scan&.infected? || piece_justificative_file.virus_scanner.infected?
|
||||
end
|
||||
|
||||
def virus_scan_pending?
|
||||
virus_scan&.pending? || piece_justificative_file.virus_scanner.pending?
|
||||
end
|
||||
|
||||
def virus_scan_no_scan?
|
||||
virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed?
|
||||
end
|
||||
|
||||
def for_api
|
||||
if piece_justificative_file.attached? && (virus_scan&.safe? || virus_scan&.pending?)
|
||||
if piece_justificative_file.attached? && (virus_scan_safe? || virus_scan_pending?)
|
||||
Rails.application.routes.url_helpers.url_for(piece_justificative_file)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_virus_scan
|
||||
if self.piece_justificative_file&.attachment&.blob.present?
|
||||
VirusScan.where(champ: self).where.not(blob_key: self.piece_justificative_file.blob.key).delete_all
|
||||
VirusScan.find_or_create_by!(champ: self, blob_key: self.piece_justificative_file.blob.key) do |virus_scan|
|
||||
virus_scan.status = VirusScan.statuses.fetch(:pending)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
module VirusScanConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
attr_reader :attachment_attribute
|
||||
|
||||
def add_virus_scan_on(piece_justificative)
|
||||
@attachment_attribute = piece_justificative
|
||||
end
|
||||
|
||||
def virus_scan
|
||||
VirusScan.find_by(blob_key: self.attachment_attribute.blob.key)
|
||||
end
|
||||
|
||||
def create_virus_scan(piece_justificative)
|
||||
if piece_justificative&.attachment&.blob.present?
|
||||
VirusScan.find_or_create_by!(blob_key: piece_justificative.blob.key) do |virus_scan|
|
||||
virus_scan.status = VirusScan.statuses.fetch(:pending)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,12 +6,4 @@ class VirusScan < ApplicationRecord
|
|||
safe: 'safe',
|
||||
infected: 'infected'
|
||||
}
|
||||
|
||||
validates :champ_id, uniqueness: { scope: :blob_key }
|
||||
|
||||
after_create :perform_scan
|
||||
|
||||
def perform_scan
|
||||
AntiVirusJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1 +1,29 @@
|
|||
ActiveStorage::Service.url_expires_in = 1.hour
|
||||
|
||||
# We want to run the virus scan on every ActiveStorage attachment,
|
||||
# regardless of its type (user-uploaded document, instructeur-uploaded attestation, form template, etc.)
|
||||
#
|
||||
# To do this, the best place to work on is the ActiveStorage::Attachment
|
||||
# objects themselves.
|
||||
#
|
||||
# We have to monkey patch ActiveStorage in order to always run an analyzer.
|
||||
# The way analyzable blob interface work is by running the first accepted analyzer.
|
||||
# This is not what we want for the virus scan. Using analyzer interface is still beneficial
|
||||
# as it takes care of downloading the blob.
|
||||
ActiveStorage::Attachment.class_eval do
|
||||
after_create_commit :virus_scan
|
||||
|
||||
private
|
||||
|
||||
def virus_scan
|
||||
ActiveStorage::VirusScanner.new(blob).analyze_later
|
||||
end
|
||||
end
|
||||
|
||||
ActiveStorage::Attached::One.class_eval do
|
||||
def virus_scanner
|
||||
if attached?
|
||||
ActiveStorage::VirusScanner.new(attachment.blob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -167,8 +167,8 @@ feature 'The user' do
|
|||
expect(page).to have_text('analyse antivirus en cours')
|
||||
|
||||
# Mark file as scanned and clean
|
||||
virus_scan = VirusScan.last
|
||||
virus_scan.update(scanned_at: Time.zone.now, status: VirusScan.statuses.fetch(:safe))
|
||||
attachment = ActiveStorage::Attachment.last
|
||||
attachment.blob.update(metadata: attachment.blob.metadata.merge(scanned_at: Time.zone.now, virus_scan_result: ActiveStorage::VirusScanner::SAFE))
|
||||
within '.piece-justificative' do
|
||||
click_on 'rafraichir'
|
||||
end
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
RSpec.describe AntiVirusJob, type: :job do
|
||||
RSpec.describe VirusScannerJob, type: :job do
|
||||
let(:champ) do
|
||||
champ = create(:champ, :piece_justificative)
|
||||
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
|
||||
champ
|
||||
end
|
||||
let(:virus_scan) { create(:virus_scan, status: VirusScan.statuses.fetch(:pending), champ: champ, blob_key: champ.piece_justificative_file.blob.key) }
|
||||
|
||||
subject { AntiVirusJob.new.perform(virus_scan) }
|
||||
subject { VirusScannerJob.new.perform(champ.piece_justificative_file.blob) }
|
||||
|
||||
context "when no virus is found" do
|
||||
let(:virus_found?) { true }
|
||||
|
@ -16,7 +15,7 @@ RSpec.describe AntiVirusJob, type: :job do
|
|||
subject
|
||||
end
|
||||
|
||||
it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:safe)) }
|
||||
it { expect(champ.piece_justificative_file.virus_scanner.safe?).to be_truthy }
|
||||
end
|
||||
|
||||
context "when a virus is found" do
|
||||
|
@ -27,6 +26,6 @@ RSpec.describe AntiVirusJob, type: :job do
|
|||
subject
|
||||
end
|
||||
|
||||
it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:infected)) }
|
||||
it { expect(champ.piece_justificative_file.virus_scanner.infected?).to be_truthy }
|
||||
end
|
||||
end
|
|
@ -383,21 +383,20 @@ describe Champ do
|
|||
let(:type_de_champ) { create(:type_de_champ_piece_justificative) }
|
||||
|
||||
context 'and there is a blob' do
|
||||
before { champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") }
|
||||
before do
|
||||
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
|
||||
champ.save
|
||||
end
|
||||
|
||||
it { expect { champ.save }.to change(VirusScan, :count).by(1) }
|
||||
it { expect(champ.piece_justificative_file.virus_scanner.analyzed?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'and there is no blob' do
|
||||
it { expect { champ.save }.to_not change(VirusScan, :count) }
|
||||
before { champ.save }
|
||||
|
||||
it { expect(champ.piece_justificative_file.virus_scanner).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type_champ is not type_de_champ_piece_justificative' do
|
||||
let(:type_de_champ) { create(:type_de_champ_textarea) }
|
||||
|
||||
it { expect { champ.save }.to_not change(VirusScan, :count) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "repetition" do
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
describe Champs::PieceJustificativeChamp do
|
||||
describe '#for_api' do
|
||||
let(:champ_pj) { create(:champ_piece_justificative) }
|
||||
let(:metadata) { champ_pj.piece_justificative_file.blob.metadata }
|
||||
|
||||
before { champ_pj.virus_scan.update(status: status) }
|
||||
before { champ_pj.piece_justificative_file.blob.update(metadata: metadata.merge(virus_scan_result: status)) }
|
||||
|
||||
subject { champ_pj.for_api }
|
||||
|
||||
context 'when file is safe' do
|
||||
let(:status) { 'safe' }
|
||||
let(:status) { ActiveStorage::VirusScanner::SAFE }
|
||||
it { is_expected.to include("/rails/active_storage/blobs/") }
|
||||
end
|
||||
|
||||
context 'when file is not scanned' do
|
||||
let(:status) { 'pending' }
|
||||
let(:status) { ActiveStorage::VirusScanner::PENDING }
|
||||
it { is_expected.to include("/rails/active_storage/blobs/") }
|
||||
end
|
||||
|
||||
context 'when file is infected' do
|
||||
let(:status) { 'infected' }
|
||||
let(:status) { ActiveStorage::VirusScanner::INFECTED }
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ describe ChampSerializer do
|
|||
|
||||
before do
|
||||
champ.piece_justificative_file.attach({ filename: __FILE__, io: File.open(__FILE__) })
|
||||
champ.reload.virus_scan.safe!
|
||||
champ.piece_justificative_file.virus_scanner.analyze_later
|
||||
end
|
||||
after { champ.piece_justificative_file.purge }
|
||||
|
||||
|
|
Loading…
Reference in a new issue