Save virus scan status to blob metadata

This commit is contained in:
Paul Chavard 2019-05-02 11:37:35 +02:00
parent 7e8df41648
commit f113d108c9
13 changed files with 143 additions and 92 deletions

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }