ActiveStorage AV Scan
A lightweight antivirus scanning extension for Rails Active Storage. Automatically scans uploaded files using ClamAV, records results, and exposes clean helpers for models and views.
⚠️ Currently an early release (
v0.1.0) — battle-tested in real Rails apps, but specs and HTTP/Lambda integrations are coming soon.
Features
- Automatic background scanning of Active Storage attachments
- Adapter-agnostic — works with Sidekiq, SolidQueue, Resque, or any ActiveJob backend
- Built-in ClamAV scanner
- Dedicated
AttachmentScanmodel to store results - Easy view helpers for showing scan status
- Configurable infection policy (
infected_handler)
🚀 Installation
Add to your Gemfile:
gem "active_storage_av_scan"Install and migrate:
bundle install
bundle exec rails g active_storage_av_scan:install
bundle exec rails db:migrateThis creates:
- The
active_storage_attachment_scanstable - An initializer at
config/initializers/active_storage_av_scan.rb
⚙️ Configuration
Default: Local ClamAV
# config/initializers/active_storage_av_scan.rb
ActiveStorageAvScan.queue_name = :av_scan
ActiveStorageAvScan.scanner = ActiveStorageAvScan::Scanners::Clamav.new💡 Check ClamAV is installed: macOS →
brew install clamavDebian/Ubuntu →sudo apt install clamav
💡 Custom Scanners
You can plug in any custom scanner backend by assigning your own object that implements the same simple scan interface:
class MyCustomScanner
include ActiveStorageAvScan::Scanner
# Must return an ActiveStorageAvScan::ScanResult
def scan(io:, filename:, blob: nil, **_opts)
# Example: call your external AV API or service
response = ExternalAV.scan(io)
if response.clean?
ActiveStorageAvScan::ScanResult.new(status: :clean, details: "Clean via ExternalAV")
else
ActiveStorageAvScan::ScanResult.new(
status: :infected,
virus_name: response.virus_name,
details: response.details
)
end
end
end
# Use it in your initializer
ActiveStorageAvScan.scanner = MyCustomScanner.newAny scanner that responds to:
scan(io:, filename:, blob: nil, **opts)and returns an instance of:
ActiveStorageAvScan::ScanResult.new(status:, virus_name:, details:)will work out of the box.
Upcoming: the gem will include first-class support for HTTP-based external scanners (e.g. AWS Lambda AV, REST APIs) in a future release.
Usage
1. Enable scanning
Add av_scanned_attachments to your model:
class Expense < ApplicationRecord
has_many_attached :receipts
av_scanned_attachments :receipts
endEvery upload triggers a background scan automatically.
2. Check results
@expense.receipts_all_clean? # => true / false
@expense.receipts_any_infected? # => true / false
@expense.receipts_scan_statuses # => [[attachment, "clean"], ...]All results are stored in ActiveStorageAvScan::AttachmentScan.
🧠 Infection Handling
You can define what happens when a file is infected:
ActiveStorageAvScan.infected_handler = lambda do |blob, scan|
Rails.logger.warn "[AV] Infected file: #{blob.filename} (#{scan.virus_name})"
blob.attachments.each(&:purge_later)
endHandler signature:
->(blob, scan_record) { ... }💻 View Helpers
Add scan badges next to your file links:
<% @expense.receipts.each do |attachment| %>
<%= link_to attachment.filename, url_for(attachment) %>
<%= av_scan_badge(attachment) %>
<% end %>Default output:
<span class="av-scan-badge av-scan-badge-clean">Clean</span>You can also override classes inline for a specific badge:
<%= av_scan_badge(attachment, css_class: "ml-2 text-xs") %>Customize badge classes for Tailwind/Bootstrap/etc.:
ActiveStorageAvScan.badge_class_map = {
"pending" => "text-gray-700 bg-gray-200 rounded px-2 py-0.5 text-xs",
"clean" => "text-green-800 bg-green-100 rounded px-2 py-0.5 text-xs",
"infected" => "text-red-800 bg-red-100 rounded px-2 py-0.5 text-xs",
"error" => "text-yellow-800 bg-yellow-100 rounded px-2 py-0.5 text-xs"
}When you run
rails g active_storage_av_scan:install, a defaultapp/assets/stylesheets/active_storage_av_scan.cssfile is created to style scan-badges. You can import it into your main stylesheet, tweak it, or replace it entirely with your own styles or Tailwind/Bootstrap classes.
🔍 Model Reference
ActiveStorageAvScan::AttachmentScan
| Column | Type | Description |
|---|---|---|
blob_id |
bigint | References ActiveStorage::Blob
|
status |
string |
"pending", "clean", "infected", "error"
|
scanner |
string | Scanner class used |
virus_name |
string | Virus signature name (if any) |
details |
text | Raw output or error message |
scanned_at |
datetime | Time scanned |
🧩 Storage Compatibility
Works transparently with all Active Storage services:
amazon:
service: S3
bucket: your-bucket-<%= Rails.env %>
google:
service: GCS
bucket: your-bucket-<%= Rails.env %>
mirror:
service: Mirror
primary: amazon
mirrors: [google]The scanner only ever sees an IO object (blob.open), regardless of backend.
🔒 Requirements
- Ruby 3.0+
- Rails 6.1+ (tested on 6.1, 7.0, 7.1)
- Active Storage configured
- ClamAV installed (
clamscanavailable in PATH)
🧰 Development
Clone and run locally:
git clone git@github.com:ajazfarhad/active_storage_av_scan.git
cd active_storage_av_scan
bundle installYou can test inside a sample Rails app:
rails g active_storage_av_scan:install
rails db:migrateAttach a file and observe the scan results in the console or DB.
🧭 Roadmap
- HTTP / Lambda scanner backend
- Async result polling & notifications
- Slack / Email alert hooks
- CLI task (
rails active_storage_av_scan:scan_all) - Lightweight audit log
- Rails Admin / ActiveAdmin integration
- RSpec test coverage (planned for v0.2.0)
🧑💻 Contributing
Contributions welcome! Bug reports and PRs on GitHub: https://github.com/ajazfarhad/active_storage_av_scan
📄 License
MIT License © 2025 [Ajaz Farhad]