Skip to content

Commit

Permalink
DEV: Modernize the Antivirus plugin.
Browse files Browse the repository at this point in the history
Autoload files, and annotate models.
  • Loading branch information
romanrizzi committed Jun 28, 2024
1 parent 6bab8a2 commit 67c08ef
Show file tree
Hide file tree
Showing 22 changed files with 120 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CreateScannedUploads < ::Jobs::Scheduled
def execute(_args)
return unless SiteSetting.discourse_antivirus_enabled?

scanner = DiscourseAntivirus::BackgroundScan.new(DiscourseAntivirus::ClamAV.instance)
scanner = DiscourseAntivirus::BackgroundScan.new(DiscourseAntivirus::ClamAv.instance)
scanner.queue_batch
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class FetchAntivirusVersion < ::Jobs::Scheduled
def execute(_args)
return unless SiteSetting.discourse_antivirus_enabled?

DiscourseAntivirus::ClamAV.instance.update_versions
DiscourseAntivirus::ClamAv.instance.update_versions
end
end
end
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ScanBatch < ::Jobs::Scheduled
def execute(_args)
return unless SiteSetting.discourse_antivirus_enabled?

antivirus = DiscourseAntivirus::ClamAV.instance
antivirus = DiscourseAntivirus::ClamAv.instance
return unless antivirus.accepting_connections?

DiscourseAntivirus::BackgroundScan.new(antivirus).scan_batch
Expand Down
37 changes: 37 additions & 0 deletions models/reviewable_upload.rb → app/models/reviewable_upload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,40 @@ def build_action(actions, id, icon:, bundle: nil, confirm: false, button_class:
end
end
end

# == Schema Information
#
# Table name: reviewables
#
# id :bigint not null, primary key
# type :string not null
# status :integer default("pending"), not null
# created_by_id :integer not null
# reviewable_by_moderator :boolean default(FALSE), not null
# reviewable_by_group_id :integer
# category_id :integer
# topic_id :integer
# score :float default(0.0), not null
# potential_spam :boolean default(FALSE), not null
# target_id :integer
# target_type :string
# target_created_by_id :integer
# payload :json
# version :integer default(0), not null
# latest_score :datetime
# created_at :datetime not null
# updated_at :datetime not null
# force_review :boolean default(FALSE), not null
# reject_reason :text
#
# Indexes
#
# idx_reviewables_score_desc_created_at_desc (score,created_at)
# index_reviewables_on_reviewable_by_group_id (reviewable_by_group_id)
# index_reviewables_on_status_and_created_at (status,created_at)
# index_reviewables_on_status_and_score (status,score)
# index_reviewables_on_status_and_type (status,type)
# index_reviewables_on_target_id_where_post_type_eq_post (target_id) WHERE ((target_type)::text = 'Post'::text)
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
#
20 changes: 20 additions & 0 deletions models/scanned_upload.rb → app/models/scanned_upload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,23 @@ def handle_scan_result(result)
result[:found] ? move_to_quarantine!(result[:message]) : save!
end
end

# == Schema Information
#
# Table name: scanned_uploads
#
# id :bigint not null, primary key
# upload_id :integer
# next_scan_at :datetime
# quarantined :boolean default(FALSE), not null
# scans :integer default(0), not null
# virus_database_version_used :integer
# created_at :datetime not null
# updated_at :datetime not null
# last_scan_failed :boolean default(FALSE), not null
# scan_result :string
#
# Indexes
#
# index_scanned_uploads_on_upload_id (upload_id) UNIQUE
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module DiscourseAntivirus
class EnableDiscourseAntivirusValidator
def initialize(opts = {})
@opts = opts
end

def valid_value?(val)
return true if val == "f"

DiscourseAntivirus::ClamAv.correctly_configured?
end

def error_message
I18n.t("site_settings.errors.antivirus_srv_record_required")
end
end
end
12 changes: 9 additions & 3 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# frozen_string_literal: true

DiscourseAntivirus::Engine.routes.draw do
root to: "antivirus#index"
get "/stats" => "antivirus#index"
#DiscourseAntivirus::Engine.routes.draw do
# root to: "antivirus#index"
# get "/stats" => "antivirus#index"
#end

Discourse::Application.routes.draw do
scope "/admin/plugins/antivirus", constraints: AdminConstraint.new do
get "/stats" => "antivirus#index"
end
end
2 changes: 1 addition & 1 deletion config/settings.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins:
discourse_antivirus_enabled:
default: false
validator: EnableDiscourseAntivirusValidator
validator: DiscourseAntivirus::EnableDiscourseAntivirusValidator
antivirus_live_scan_images:
default: false
antivirus_srv_record:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true

module DiscourseAntivirus
class ClamAV
class ClamAv
VIRUS_FOUND = Class.new(StandardError)
PLUGIN_NAME = "discourse-antivirus"
STORE_KEY = "clamav-versions"
DOWNLOAD_FAILED = "Download failed"
UNAVAILABLE = "unavailable"

def self.instance
new(Discourse.store, DiscourseAntivirus::ClamAVServicesPool.new)
new(Discourse.store, DiscourseAntivirus::ClamAvServicesPool.new)
end

def self.correctly_configured?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module DiscourseAntivirus
class ClamAVHealthMetric < ::DiscoursePrometheus::InternalMetric::Custom
class ClamAvHealthMetric < ::DiscoursePrometheus::InternalMetric::Custom
attribute :name, :labels, :description, :value, :type

def initialize
Expand All @@ -15,7 +15,7 @@ def collect
last_check = @@clamav_stats[:last_check]

if (!last_check || should_recheck?(last_check))
antivirus = DiscourseAntivirus::ClamAV.instance
antivirus = DiscourseAntivirus::ClamAv.instance
available = antivirus.accepting_connections? ? 1 : 0

@@clamav_stats[:status] = available
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module DiscourseAntivirus
class ClamAVService
class ClamAvService
def initialize(connection_factory, hostname, port)
@connection_factory = connection_factory
@hostname = hostname
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module DiscourseAntivirus
class ClamAVServicesPool
class ClamAvServicesPool
def online_services
instances.select(&:online?)
end
Expand Down Expand Up @@ -31,7 +31,7 @@ def instances
@instances ||=
servers
.filter { |server| server&.hostname.present? && server&.port.present? }
.map { |server| ClamAVService.new(connection_factory, server.hostname, server.port) }
.map { |server| ClamAvService.new(connection_factory, server.hostname, server.port) }
end

def servers
Expand Down
13 changes: 3 additions & 10 deletions lib/discourse_antivirus/engine.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
# frozen_string_literal: true

module DiscourseAntivirus
module ::DiscourseAntivirus
class Engine < ::Rails::Engine
engine_name "discourse-antivirus"
engine_name PLUGIN_NAME
isolate_namespace DiscourseAntivirus

config.after_initialize do
Discourse::Application.routes.append do
mount ::DiscourseAntivirus::Engine,
at: "/admin/plugins/antivirus",
constraints: AdminConstraint.new
end
end
config.autoload_paths << File.join(config.root, "lib")
end
end
6 changes: 0 additions & 6 deletions lib/discourse_antivirus_constraint.rb

This file was deleted.

17 changes: 0 additions & 17 deletions lib/validators/enable_discourse_antivirus_validator.rb

This file was deleted.

30 changes: 10 additions & 20 deletions plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,18 @@
gem "dns-sd", "0.1.3"

enabled_site_setting :discourse_antivirus_enabled

register_asset "stylesheets/reviewable-upload.scss"

module ::DiscourseAntivirus
PLUGIN_NAME = "discourse-antivirus"
end

require_relative "lib/discourse_antivirus/engine"
require_relative "lib/validators/enable_discourse_antivirus_validator"

add_admin_route "antivirus.title", "antivirus"

after_initialize do
require_relative "app/controllers/discourse_antivirus/antivirus_controller.rb"
require_relative "lib/discourse_antivirus/clamav_services_pool.rb"
require_relative "lib/discourse_antivirus/clamav_service.rb"
require_relative "lib/discourse_antivirus/clamav.rb"
require_relative "lib/discourse_antivirus/background_scan.rb"
require_relative "models/scanned_upload.rb"
require_relative "models/reviewable_upload.rb"
require_relative "serializers/reviewable_upload_serializer.rb"
require_relative "jobs/scheduled/scan_batch.rb"
require_relative "jobs/scheduled/create_scanned_uploads.rb"
require_relative "jobs/scheduled/fetch_antivirus_version.rb"
require_relative "jobs/scheduled/remove_orphaned_scanned_uploads.rb"
require_relative "jobs/scheduled/flag_quarantined_uploads.rb"

register_reviewable_type ReviewableUpload

add_to_serializer(
Expand All @@ -40,8 +30,8 @@
include_condition: -> { SiteSetting.discourse_antivirus_enabled? && scope.is_staff? },
) do
!!PluginStore.get(
DiscourseAntivirus::ClamAV::PLUGIN_NAME,
DiscourseAntivirus::ClamAV::UNAVAILABLE,
DiscourseAntivirus::ClamAv::PLUGIN_NAME,
DiscourseAntivirus::ClamAv::UNAVAILABLE,
)
end

Expand All @@ -53,7 +43,7 @@
should_scan_file = !upload.for_export && (!is_image || SiteSetting.antivirus_live_scan_images)

if validate && should_scan_file && upload.valid?
antivirus = DiscourseAntivirus::ClamAV.instance
antivirus = DiscourseAntivirus::ClamAv.instance

response = antivirus.scan_file(file)
is_positive = response[:found]
Expand All @@ -63,9 +53,9 @@
end

if defined?(::DiscoursePrometheus)
require_relative "lib/discourse_antivirus/clamav_health_metric.rb"
require_relative "lib/discourse_antivirus/clam_av_health_metric.rb"

DiscoursePluginRegistry.register_global_collector(DiscourseAntivirus::ClamAVHealthMetric, self)
DiscoursePluginRegistry.register_global_collector(DiscourseAntivirus::ClamAvHealthMetric, self)
end

add_reviewable_score_link(:malicious_file, "plugin:discourse-antivirus")
Expand Down
14 changes: 7 additions & 7 deletions spec/lib/discourse_antivirus/background_scan_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
updated_at: "Wed Jun 24 10:13:27 2020",
}
PluginStore.set(
DiscourseAntivirus::ClamAV::PLUGIN_NAME,
DiscourseAntivirus::ClamAV::STORE_KEY,
DiscourseAntivirus::ClamAv::PLUGIN_NAME,
DiscourseAntivirus::ClamAv::STORE_KEY,
[version_data],
)
end
Expand Down Expand Up @@ -55,15 +55,15 @@
.with(upload, max_file_size_kb: filesize)
.raises(OpenURI::HTTPError.new("forbidden", nil))

antivirus = DiscourseAntivirus::ClamAV.new(store, build_fake_pool(socket))
antivirus = DiscourseAntivirus::ClamAv.new(store, build_fake_pool(socket))
scanner = described_class.new(antivirus)
scanned_upload = ScannedUpload.create_new!(upload)

scanner.scan([scanned_upload])
scanned_upload.reload

expect(scanned_upload.scans).to eq(0)
expect(scanned_upload.scan_result).to eq(DiscourseAntivirus::ClamAV::DOWNLOAD_FAILED)
expect(scanned_upload.scan_result).to eq(DiscourseAntivirus::ClamAv::DOWNLOAD_FAILED)
expect(scanned_upload.next_scan_at).to be_present
expect(scanned_upload.last_scan_failed).to eq(true)
end
Expand All @@ -75,15 +75,15 @@
filesize = upload.filesize + 2.megabytes
store.expects(:download).with(upload, max_file_size_kb: filesize).returns(nil)

antivirus = DiscourseAntivirus::ClamAV.new(store, build_fake_pool(socket))
antivirus = DiscourseAntivirus::ClamAv.new(store, build_fake_pool(socket))
scanner = described_class.new(antivirus)
scanned_upload = ScannedUpload.create_new!(upload)

scanner.scan([scanned_upload])
scanned_upload.reload

expect(scanned_upload.scans).to eq(0)
expect(scanned_upload.scan_result).to eq(DiscourseAntivirus::ClamAV::DOWNLOAD_FAILED)
expect(scanned_upload.scan_result).to eq(DiscourseAntivirus::ClamAv::DOWNLOAD_FAILED)
expect(scanned_upload.next_scan_at).to be_present
expect(scanned_upload.last_scan_failed).to eq(true)
end
Expand Down Expand Up @@ -272,7 +272,7 @@ def build_fake_pool(socket)
def build_scanner(quarantine_files: false)
IO.stubs(:select)
socket = quarantine_files ? FakeTCPSocket.positive : FakeTCPSocket.negative
antivirus = DiscourseAntivirus::ClamAV.new(Discourse.store, build_fake_pool(socket))
antivirus = DiscourseAntivirus::ClamAv.new(Discourse.store, build_fake_pool(socket))
described_class.new(antivirus)
end
end
2 changes: 1 addition & 1 deletion spec/lib/discourse_antivirus/clamav_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require_relative "../../support/fake_pool"
require_relative "../../support/fake_tcp_socket"

describe DiscourseAntivirus::ClamAV do
describe DiscourseAntivirus::ClamAv do
fab!(:upload) { Fabricate(:image_upload) }
let(:file) { File.open(Discourse.store.path_for(upload)) }

Expand Down
4 changes: 2 additions & 2 deletions spec/plugin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
def mock_antivirus(socket)
IO.stubs(:select).returns(true)
pool = FakePool.new([FakeTCPSocket.online, socket])
antivirus = DiscourseAntivirus::ClamAV.new(Discourse.store, pool)
DiscourseAntivirus::ClamAV.expects(:instance).returns(antivirus)
antivirus = DiscourseAntivirus::ClamAv.new(Discourse.store, pool)
DiscourseAntivirus::ClamAv.expects(:instance).returns(antivirus)
end
end
2 changes: 1 addition & 1 deletion spec/support/fake_pool.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class FakePool < DiscourseAntivirus::ClamAVServicesPool
class FakePool < DiscourseAntivirus::ClamAvServicesPool
def initialize(sockets)
@sockets = sockets
@connections = 0
Expand Down

0 comments on commit 67c08ef

Please sign in to comment.